Adding a new page to Fable Elmish, Part 1
Creating a new page
Start with the new project you created from the previous post. This example will reuse the existing page so that we can focus on just adding the page to the project. First, navigate to the src folder and create a new folder named NewInfo. Then go into the Info folder and copy View.fs and paste it inside the NewInfo folder. Your folder structure should look like this
For the page to compiles properly, you need to add the page to your project. Open up the .fsproj in the root folder of your project and add the following code inside the <ItemGroup></ItemGroup>
tag.
<Compile Include="src/NewInfo/View.fs" />
This is important. The .fsproj file shows the compile order of every files in the project. This means that the compiler compiles the first file you see first and then second. You can see the full content of my .fsproj file below
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- Global to the app -->
<Compile Include="Global.fs" />
<!-- Info -->
<Compile Include="Info/View.fs" />
<!-- New Info -->
<Compile Include="NewInfo/View.fs" />
<!-- Counter -->
<Compile Include="Counter/Types.fs" />
<Compile Include="Counter/State.fs" />
<Compile Include="Counter/View.fs" />
<!-- Home -->
<Compile Include="Home/Types.fs" />
<Compile Include="Home/State.fs" />
<Compile Include="Home/View.fs" />
<!-- Navbar -->
<Compile Include="Navbar/View.fs" />
<!-- App -->
<Compile Include="Types.fs" />
<Compile Include="State.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<Import Project="..\.paket\Paket.Restore.targets" />
</Project>
This tell you that the compiler compiles Global.fs first and then Info/View.fs. In general, you will want to add your file after
<!-- Global to the app -->
<Compile Include="src/Global.fs" />
and before
<!-- App -->
<Compile Include="src/Types.fs" />
<Compile Include="src/State.fs" />
<Compile Include="src/App.fs" />
Also, if your new page require code from another file, you need to add the file first and then your page. You might noticed that I include in the code above. This is a comment and it is optional for you to add it in your code.
Now open the NewInfo/View.fs file and at the first line where it said
module Info.View
and replace it with
module NewInfo.View
This change the domain of your file from Info.View to NewInfo.View. Then modify the page so that you can tell the difference from the info page. Change the following code
let root =
div
[ ClassName "content" ]
[ h1
[ ]
[ str "About page" ]
p
[ ]
[ str "This template is a simple application build with Fable + Elmish + React." ] ]
to
let root =
div
[ ClassName "content" ]
[ h1
[ ]
[ str "New page" ]
p
[ ]
[ str "You created a new page" ]
The root function return the html that will be shown when the page is loaded. What you just did is changing the header to display “New Page” instead of “About Page”
Setting up the Handlers
You will now setup a link to your new page and create the code that serves it. First open up src/Global.fs and edit
type Page =
| Home
| Counter
| About
let toHash page =
match page with
| About -> "#about"
| Counter -> "#counter"
| Home -> "#home"
to this
type Page =
| Home
| Counter
| About
| NewPage
let toHash page =
match page with
| About -> "#about"
| Counter -> "#counter"
| Home -> "#home"
| NewPage -> "#newpage"
Be aware that I made two changes. I add a NewPage union case to Page. That mean that Page could be either Home, Counter, About, or NewPage. I call it NewPage to point out that you do not need to name it the same as your page folder which is called NewInfo.
Then I add a new pattern rule in the toHash function for the new union case. The “#newpage” represent what get added to the url when you open the page. For example, clicking on the Home link will change the url to http://localhost:8080/#home. In summary, you add a new type of Page called NewPage and that the url for it is “#newpage”
Now to add your link to the side menu. First open up src/App.fs and edit
let menu currentPage =
aside
[ ClassName "menu" ]
[ p
[ ClassName "menu-label" ]
[ str "General" ]
ul
[ ClassName "menu-list" ]
[ menuItem "Home" Home currentPage
menuItem "Counter sample" Counter currentPage
menuItem "About" Page.About currentPage ] ]
so that it look like this
let menu currentPage =
aside
[ ClassName "menu" ]
[ p
[ ClassName "menu-label" ]
[ str "General" ]
ul
[ ClassName "menu-list" ]
[ menuItem "Home" Home currentPage
menuItem "Counter sample" Counter currentPage
menuItem "About" Page.About currentPage
menuItem "New Page" Page.NewPage currentPage ] ]
This will add a link to your page inside the side menu. Watch out for the closing ]] and make sure you didn’t duplicate them or put them at the wrong spot. The function menuItem is part of the template and it creates a link with the text “New Page” and it link to Page of type NewPage.
The currentPage
is so that the link would looks different when you are already looking at that page. I included the menuItem function code below so you can how it work
let menuItem label page currentPage =
li
[ ]
[ a
[ classList [ "is-active", page = currentPage ]
Href (toHash page) ]
[ str label ] ]
Still inside App.fs, edit your root function from
let root model dispatch =
let pageHtml page =
match page with
| Page.About -> Info.View.root
| Counter -> Counter.View.root model.Counter (CounterMsg >> dispatch)
| Home -> Home.View.root model.Home (HomeMsg >> dispatch)
to
let root model dispatch =
let pageHtml page =
match page with
| Page.About -> Info.View.root
| Counter -> Counter.View.root model.Counter (CounterMsg >> dispatch)
| Home -> Home.View.root model.Home (HomeMsg >> dispatch)
| NewPage -> NewInfo.View.root
Looking at the changes closely, I add a pattern rule for the NewPage union case I created. This code is checking the Page that it is getting and depending on the kind of Page, it calls a root function from different module. Now remember earlier, the top line of your NewInfo/View.fs was changed to
module NewInfo.View
This means that your module is NewInfo.View. Also inside the NewInfo/View.fs file is a root function.
To copy the other code, you call your root function with the module name to get NewInfo.View.root
Finally open up src/State.fs and modify
let pageParser: Parser<Page->Page,Page> =
oneOf [
map About (s "about")
map Counter (s "counter")
map Home (s "home")
]
to
let pageParser: Parser<Page->Page,Page> =
oneOf [
map About (s "about")
map Counter (s "counter")
map Home (s "home")
map NewPage (s "newpage")
]
Now open your console with the directory at the root of your project folder. Run the following command and navigate to http://localhost:8080/ and click on the “New Page” link on the side to see your new page.
npx webpack-dev-server
The final code can be found here.