UPDATE Jan 18, 2019
Thank you to everyone who showed interest in this project. For those looking for updates, please see the Bolero project, which has already completed and surpassed my goals for trail. I do not currently have any plans to continue working on this project and recommend all interested parties to contribute to Bolero instead.
- F# running in the browser on WebAssembly!
- Domain-specific language for Blazor components similar to those provided by Fable and WebSharper.
Trail.Componenttype 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
- Follow the instructions for getting started with Blazor.
- Clone this repository and run
dotnet run --project sample/standalone/BlazorApp1or 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.5.1" /> <PackageReference Include="Trail" Version="0.*" /> </ItemGroup>
5. Begin adding
Trail.Components to the library. See below.
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
Trail.Component is an abstract class inheriting from a
It expects an implementation of the
Render method and handles the rest for you.
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).
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.
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
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
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
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.
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
[<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
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 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
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
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
MyMsg, or the reducer types and function. For those details,
see the sample above or the
- 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.
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.