Trail
Packages
Package | NuGet | Downloads |
---|---|---|
Trail | ||
Trail.BlazorRedux |
Features
- F# running in the browser on WebAssembly!
- Domain-specific language for Blazor components similar to those provided by Fable and WebSharper.
Trail.Component
type to make it easier to create components with the DSL.- Sample application demonstrating the capabilities based on the stand-alone Blazor template.
- Can write nearly the entire app in F#! All you need is a
csproj
for theProgram.cs
and assets.
Getting Started
- Follow the instructions for getting started with Blazor.
- Clone this repository and run
dotnet run --project sample/standalone/BlazorApp1
or run from the latest Visual Studio 2017 Preview. - You can also create a new Blazor project,
- Add an F# library project,
- Add the following libraries to the F# library project:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Browser" Version="0.2.0" PrivateAssets="all" />
<PackageReference Include="Trail" Version="0.*" />
</ItemGroup>
, and
5. Begin adding Trail.Component
s to the library. See below.
App
An application needs a router to discover and provide navigation to all the pages. An App
component may look like the following in Trail:
namespace BlazorApp1
open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open System.Net.Http
open Microsoft.AspNetCore.Blazor
open Microsoft.AspNetCore.Blazor.Browser.Rendering
open Microsoft.AspNetCore.Blazor.Browser.Services
open Microsoft.AspNetCore.Blazor.Components
open Microsoft.AspNetCore.Blazor.Layouts
open Microsoft.AspNetCore.Blazor.Routing
open BlazorApp1
open BlazorApp1.Shared
open Trail
type App() =
inherit Trail.Component()
override __.Render() =
Dom.router<Router> typeof<App>.Assembly
A Trail.Component
is an abstract class inheriting from a BlazorComponent
.
It expects an implementation of the Render
method and handles the rest for you.
Index
The sample app provides several pages, including the index page, which just has some text and a custom Blazor component. The code for the index page looks like this:
namespace BlazorApp1.Pages
open System
open System.Collections.Generic
open System.Linq
open System.Threading.Tasks
open System.Net.Http
open Microsoft.AspNetCore.Blazor
open Microsoft.AspNetCore.Blazor.Components
open Microsoft.AspNetCore.Blazor.Layouts
open Microsoft.AspNetCore.Blazor.Routing
open BlazorApp1
open BlazorApp1.Shared
open Trail
[<LayoutAttribute(typeof<MainLayout>)>]
[<RouteAttribute("/")>]
type Index () =
inherit Trail.Component()
override __.Render() =
Dom.Fragment [
Dom.h1 [] [ Dom.text "Hello, world!" ]
Dom.text "\n\nWelcome to your new app.\n\n"
Dom.comp<SurveyPrompt> [Dom.HtmlAttribute("title", "How is Blazor working for you?")] []
]
This is a bit more involved, as it includes several DOM nodes, as well as several attributes. These attributes identify this component as a page component. Child components will not require these attributes. (Refer to the Blazor repo for more accurate information, as this is still rapidly changing.)
The attributes are Blazor attributes for routing and applying a layout. (We'll look at the MainLayout
next.) As you can see, the RouteAttribute
specifies that the index page should be found at the site root (naturally).
The Render
method is then implemented with a Dom.Fragment
. This keeps things simple on the processing side, as we know we'll always have all the elements wrapped up in a single node. The Dom.Fragment
is not rendered in any way and works very similar to the fragment support in React.
Finally, you can see Trail already provides several helper elements, e.g. h1
, text
, and comp<'T>
. What's a comp<'T>
? As you may suspect, this is a way of rendering a custom Blazor component. We'll look at the SurveyPrompt
after the MainLayout
.
Aside: You may be wondering, "What's with all the namespaces?" The namespaces are repeated because, at least in my experiments, I found that the web assembly compiler seems to need those opened in order to compile properly. We may be able to reduce them a bit now that it's all in F#.
Shared
There are several shared components, including the navigation menu, the main layout, and the survey. You can find these in the Shared.fs
file in the sample folder.
type MainLayout () =
inherit Trail.Component()
override this.Render() =
Dom.div [Dom.HtmlAttribute("class", "container-fluid")] [
Dom.div [Dom.HtmlAttribute("class", "row")] [
Dom.div [Dom.HtmlAttribute("class", "col-sm-3")] [
Dom.comp<NavMenu> [] []
]
Dom.div [Dom.HtmlAttribute("class", "col-sm-9")] [
Dom.content this.Body
]
]
]
member val Body : RenderFragment = Unchecked.defaultof<RenderFragment> with get, set
interface ILayoutComponent with
member this.Body with get() = this.Body
and set(value) = this.Body <- value
This is the MainLayout
. You can see that it uses the NavMenu
component and has a Body
typed as a RenderFragment
. Your page component is rendered in the Body
of the Layout
as defined by the use of the ILayoutComponent
interface.
type SurveyPrompt () =
inherit Trail.Component()
override this.Render() =
Dom.div [Dom.HtmlAttribute("class", "alert alert-survey"); Dom.HtmlAttribute("role", "alert")] [
Dom.span [Dom.HtmlAttribute("class", "glyphicon glyphicon-ok-circle"); Dom.HtmlAttribute("aria-hidden", "true")] []
Dom.strong [] [Dom.text this.Title]
Dom.text "Please take our "
Dom.a [Dom.HtmlAttribute("target", "_blank"); Dom.HtmlAttribute("class", "alert-link"); Dom.HtmlAttribute("href", "https://go.microsoft.com/fwlink/?linkid=870381")] [
Dom.text "brief survey"
]
Dom.text " and tell us what you think."
]
// This is to demonstrate how a parent component can supply parameters
member val Title : string = Unchecked.defaultof<string> with get, set
The SurveyPrompt
component doesn't have any attributes or special interfaces. However, it does provide a property that may be filled in where it is used. Be careful, however, with casing. Here's where we specified the Title
property above:
Dom.comp<SurveyPrompt> [Dom.HtmlAttribute("title", "How is Blazor working for you?")] []
Note that the attribute name is lowercase. This is an area where I think we can improve type-safety with the DSL.
Blazor-Redux
Trail provids Blazor-Redux component integration, as well.
Follow the instructions in the Blazor-Redux README
to learn how to use that library. The primary change to use Trail is to convert your Trail.Component
into a
Trail.ReduxComponent
. You will need to create a base component for your application,
just as in the Blazor-Redux example, only it should be a Trail.ReduxComponent
:
[<AbstractClass>]
type MyAppComponent() =
inherit Trail.ReduxComponent<MyModel, MyMsg>()
Note that this component has an [<AbstractClass>]
attribute to indicate that it must be implemented. This is to avoid
having to provide an implementation of the Render
member.
Counter
The Counter
component looks much like the one above, only you need to call this.Dispatch
to dispatch the action,
rather than handling directly within the component:
[<Layout(typeof<MainLayout>)>]
[<Route("/counter")>]
type Counter () =
inherit MyAppComponent()
override this.Render() =
Dom.Fragment [
Dom.h1 [] [Dom.text "Counter"]
Dom.p [] [
Dom.text "Current count: "
Dom.textf "%i" this.State.Count
]
Dom.button [
Attr.onclick(fun _ -> this.Dispatch(MyMsg.IncrementByOne))
] [
Dom.text "Click me"
]
]
FetchData
The FetchData
component is also very similar to the standard FetchData
component above:
[<Layout(typeof<MainLayout>)>]
[<Route("/fetchdata")>]
type FetchData () =
inherit MyAppComponent()
override this.Render() =
Dom.Fragment [
yield Dom.h1 [] [Dom.text "Weather forecast"]
yield Dom.p [] [Dom.text "This component domonstrates fetching data from the server."]
match this.State.Forecasts with
| None | Some [||] ->
yield Dom.p [] [
Dom.em [] [Dom.text "Loading..."]
]
| Some forecasts ->
yield Dom.table [Dom.HtmlAttribute("class", "table")] [
Dom.thead [] [
Dom.tr [] [
Dom.th [] [Dom.text "Date"]
Dom.th [] [Dom.text "Temp. (C)"]
Dom.th [] [Dom.text "Temp. (F)"]
Dom.th [] [Dom.text "Summary"]
]
]
Dom.tbody [] [
for forecast in forecasts ->
Dom.tr [] [
Dom.td [] [Dom.text (forecast.Date.ToShortDateString())]
Dom.td [] [Dom.textf "%i" forecast.TemperatureC]
Dom.td [] [Dom.textf "%i" forecast.TemperatureF]
Dom.td [] [Dom.text forecast.Summary]
]
]
]
]
override this.OnInitAsync() =
ActionCreators.LoadWeather(BlazorRedux.Dispatcher this.Store.Dispatch, this.Http)
[<Inject>]
member val private Http : HttpClient = Unchecked.defaultof<HttpClient> with get, set
Here, we use the ActionCreators.LoadWeather
, as seen in the Blazor-Redux example.
You must specify a BlazorRedux.Dispatcher
delegate using this.Store.Dispatch
method from the
Trail.ReduxComponent
, as well as the injected this.Http
HttpClient
instance.
ReduxDevTools
Blazor-Redux integrates with the Redux DevTools, and you can add this
integration to your Trail.BlazorRedux
app by rendering the BlazorRedux.ReduxDevTools
component. In the sample,
I've added the component to the App
:
type App() =
inherit MyAppComponent()
override __.Render() =
Dom.Fragment [
Dom.router<Router> typeof<App>.Assembly
Dom.comp<BlazorRedux.ReduxDevTools> [] []
]
You can find the stand-alone sample here.
NOTE: this README does not cover the creation of the MyModel
, MyMsg
, or the reducer types and function. For those details,
see the sample above or the
Blazor-Redux README.
Roadmap
- More documentation, samples, and tutorials!
- Extend and improve markup helper DSL
- Test and optimize performance
- Create dotnet new templates
- Keep up with ASP.NET Blazor team
- All F# Blazor app - doesn't seem possible yet.
Contributing
Trail is very early and building on top of Blazor, which is also very early. Expect many breaking changes to come. I would love to have your help. If you have ideas, run into issues, or want to tweak or extend the DSL, please submit issues or pull requests.