Allowed.Svelte.NET is a framework that provides a bridge between Svelte and ASP.NET Core to build fast and efficient web applications.
The framework was inspired by Blazor and Svelte. While Blazor is a great platform for front-end development, both of its models - Blazor WebAssembly and Blazor Server - have some critical disadvantages that cannot be resolved in the near future. On the other hand, Svelte offers a similar syntax to Blazor and is a great way for .NET developers with experience in Razor syntax to create great web apps.
Key feature of Allowed.Svelte.NET is its ability to prerender pages from .NET using Svelte. This means that pages are generated on the server-side before being sent to the client, resulting in faster load times and a smoother user experience. Additionally, this approach can improve search engine optimization (SEO), as search engines are better able to index prerendered pages.
Allowed.Svelte.NET includes a routing system that generates a route map based on [HttpGet]
attributes. This approach
is similar to that used in Blazor, making it easy for developers to create dynamic and responsive web applications. It
fully represents the route map to the frontend, so there is no need to duplicate client-side routing logic.
dotnet tool update -g Allowed.Svelte.NET.Tools --version 1.0.2
dotnet new install Allowed.Svelte.NET.Templates::1.0.3
dotnet new svelte.net
dotnet run
This C# method is defining an action to handle a request for the "about" page in a Svelte application. The method is
decorated with the SvelteRoute
attribute, which specifies the URL path to the page ("/about"), the page name ("About")
, and the layout name ("MainLayout").
[SvelteRoute("/about/{section}", Page = "About", Layout = "MainLayout")]
public SvelteView<object> About()
{
return new SvelteView<object>(new
{
Text = "Information about this framework"
});
}
The method returns a SvelteView
object, which represents a pre-rendered view of the page. This model can
be accessed in the client-side code of the page.
In the C# code, the section
parameter can be used to process data for pre-rendering the page based on this parameter.
In the client-side code, the parameter can be accessed using export let section: string;
.
To transfer data from the server (ASP.NET Core) to the client (Svelte), you can use the IServerSideData interface
and implement the Get
method to retrieve the data from the server.
Here's an example implementation using the ConfigurationData
class:
public class ConfigurationData<T> : IServerSideData
where T : class
{
public string Section => "Configuration";
private readonly string _sectionName;
public ConfigurationData(string sectionName = "ClientData")
{
_sectionName = sectionName;
}
public Task<object?> Get(HttpContext httpContext)
{
return Task.FromResult<object?>(
httpContext.RequestServices.GetRequiredService<IConfiguration>().GetSection(_sectionName).Get<T>());
}
}
To add the ConfigurationData
implementation to the DI container in ASP.NET Core, use the AddServerSideData
extension
method provided by the Allowed.Svelte.NET framework:
builder.Services.AddServerSideData(new ConfigurationData<T>("sectionName"));
Here, T
is the type of data you want to transfer, and sectionName
is the name of the configuration section where the
data is located.
Here is an example of accessing transferred data in Svelte:
<script lang="ts">
import {ServerData} from "svelte-dotnet";
import {page} from "../stores/RouterStores";
class Configuration {
apiConnection: string = '';
}
class AppServerData extends ServerData {
configuration: Configuration = new Configuration();
}
$: data = <AppServerData>$page.data;
</script>
Overall, this allows for easy transfer of server-side data to the client, which can be useful for providing the client with necessary information such as feature flags or application settings.
There are three main stores used in Svelte.NET: UrlStore, PageStore, and RouteStore.
The UrlStore
is a Svelte store that extends the Readable
store, which provides a subscribe method to observe
changes to the URL. The UrlStore
also provides a navigate method to navigate without a reload from JavaScript.
Here is the UrlStore
interface:
interface UrlStore extends Readable<URL> {
navigate?: {
(url: string): void;
};
}
This store is used in the Router component and can be created like this:
url = createUrlStore(ssrUrl);
However, in the template project, the creation of the UrlStore
is already implemented in the RouterStores.ts
file.
The PageStore
is a Svelte store that provides access to the data model passed from the C# code. The data model is
contained in a SvelteView
object that is returned by the server-side action method, as shown in the C# code you
provided earlier.
interface PageStore extends Writable<PageStoreData> {
isDataModelEmpty: {
(): boolean;
};
updateDataModel: {
(init?: RequestInit): Promise<void>;
};
updateIsEmpty: {
(init?: RequestInit): Promise<void>;
};
}
On the client-side, the PageStore
is implemented as an interface PageStore
which extends the Writable
store
interface. It includes the following methods:
isDataModelEmpty
: This method returns a boolean value indicating whether the data model is empty. The data model can
be empty if we redirect to another URL on the client-side or if the SvelteView
model is empty.updateDataModel
: This method updates the PageStore
data model. It makes a request similar to the request used to
get the Svelte page, but it passes a header svdn-data-only="1"
, indicating that this is an API request and that the
API should return JSON data (model) instead of HTML. The init parameter is an optional variable of type RequestInit
,
with a default value of defaultRequestInit
.updateIsEmpty
: This method calls updateDataModel
if isDataModelEmpty
is true.export class PageStoreData {
isSSR: boolean;
href: string;
data: ServerData;
constructor(ssrData: ServerData);
}
The PageStoreData
class defines the shape of the data stored in the PageStore
. It includes the following properties:
isSSR
: a boolean value indicating whether the page was rendered on the server-side.href
: a string containing the current URL.data
: an instance of the ServerData class containing the data model passed from the server.export class ServerData {
model: object | undefined;
}
The ServerData class defines the shape of the server-side data passed to the PageStore. It includes the following property:
model
: an optional object containing the data model.IServerSideData
interface.PageStore
is optional for routing, but very useful for pre-rendering:
page = createPageStore(ssrData, url);
The template project already contains a PageStore
creation in the RouterStores.ts
file.
export class RouteData {
component: object | undefined;
layout: object | undefined;
constructor(component: object | undefined, layout: object | undefined = undefined);
}
class RouteStoreData {
path: string | undefined;
route: RouteData | undefined;
constructor(path?: string | undefined, route?: RouteData | undefined);
}
The RouteStoreData
class is a store that defines the current page by storing information about the route and path. It
has two properties:
path
: a string that represents the route template for the current page.route
: a RouteData
object that contains information about the current route.The RouteData
class contains two properties:
component
: an object that represents the Svelte component for the current page.layout
: an object that represents the Svelte layout component for the current page.
<script lang="ts">
import {Router, RouteView, PageTitle, LayoutView} from "svelte-dotnet";
import {url, route} from "./stores/RouterStores";
import MainLayout from "./layouts/MainLayout.svelte";
</script>
<Router route="{$route}">
<div slot="found">
<RouteView route="{$route}" url="{$url}" defaultLayout="{MainLayout}"/>
</div>
<div slot="not-found">
<PageTitle value="Not found"/>
<LayoutView layout="{MainLayout}">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</div>
</Router>
The Router
component is a Svelte component that allows for declarative routing. It provides a way to map URLs
to Svelte components and layouts.
The component takes a route prop and url prop.
Inside the Router
component, there are two slots - found
and not-found
. The found
slot is rendered when a route
is matched, and the not-found
slot is rendered when no matching route is found.
Inner components:
RouteView
component is used to render the Svelte component for the current route. It takes the route and url
props, and a defaultLayout
prop that specifies the layout to use if none is specified in the route data.LayoutView
component is used to render the layout for the current route. It takes the layout prop, which is an
optional reference to the layout component to use.PageTitle
component is used to set the document title for the current page.Overall, the Router
component provides a convenient way to handle routing in Svelte applications, similar to
the Blazor router.
A layout component is a wrapper component that defines the common structure and behavior for a group of related pages. It typically includes common elements such as a header, a footer, a navigation menu, and a container for the main content of the page.
Here's example of layout:
<div class="main-layout">
<slot/>
</div>
When a page is rendered within a layout component, its content is inserted into the slot element of the layout component.
To better understand the similarities between Blazor and Svelte, let's compare some examples:
Blazor:
<h1>Counter</h1>
<p>Current count: @count</p>
<button @onclick="HandleClick">Click me</button>
@code {
private int count = 0;
private void HandleClick()
{
count++;
}
}
Svelte:
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<h1>Counter</h1>
<p>Current count: {count}</p>
<button on:click={handleClick}>Click me</button>
We used conditional logic in the view part only as an example. It's generally better to divide the logic in the if block into the script or code-behind for better maintainability and readability.
Blazor:
<ul>
@foreach (string day in Days)
{
@if (day.StartsWith("S"))
{
<li>@day</li>
}
}
</ul>
@code {
private string[] Days = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
}
Svelte:
<script>
let days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
</script>
<ul>
{#each days as day}
{#if day.startsWith('S')}
<li>{day}</li>
{/if}
{/each}
</ul>