Hi Derek, this is actually quite easy to do. First, have a look at one of the standard templates - the client-server app template:

1
dotnet new websharper-web -lang f# -n MyProject

Here, you can see that a master template (main.html) is used (note the type provider call near the top of the file: type MainTemplate = Templating.Template<"Main.html">), into which content is added when requested at different endpoints (Site.fs).

If you don't want to inline your HTML content in F# code, you can put it into an HTML file (or main.html), mark the relevant parts as templates (with ws-template or ws-children-template), and bring them in the same way.

I recently wrote a blog article about First steps: Using HTML templates, accessing form values, and wiring events, you might find this useful too, especially if you are looking to develop SPAs.

I hope this helps.

By on 5/5/2018 11:45 AM ()

Hi Adam,

I think I have worked it out.

I now have

1
2
type MainTemplate = Templating.Template<"Main.html">
type LoginTemplate = Template<"Login.html">    

... let LoginWidget = LoginTemplate.Login().Doc()

1
2
3
4
5
let LoginPage ctx =
    Templating.Main ctx EndPoint.Login "Login" [
        h1 [] [text "Login"]
        Templating.LoginWidget
    ]

and that gets the content of the login html into the body of main.

Is this the correct approach? I am yet to try to wire up the login fields and button actions :-)

should I put the code for the login.html in it's own login.fs file (is this the recommended way?)

and is this use of main.html as a master page the way you would recommend structuring a multi-page app?

thanks so much Derek

By on 5/5/2018 5:28 PM ()

Yep, it works pretty much like that. You will find that you rarely have just static content to plug in, and more likely you will want to take some arguments to your widgets, which in turn will instantiate the inner templates before sealing them off with .Doc().

As for code organization, use whatever works best - for simple inner templates, you might just use a single .fs file, for widgets/templates involving more complex logic it might make more sense to break out into separate files.

And finally, what you have is precisely how you would implement multi-page apps whose pages are based on the same template.

By on 5/5/2018 8:08 PM ()

Thanks again Adam, sorry this is a long message. I am making progress, though. I removed the outer div:

1
2
3
<div ws-template="Login" class="login-wrapper">
// login html
</div>

as with that in place

1
2
  let LoginWidget () = 
        LoginTemplate.Login().Doc()  

gives an error saying "method or object constructor 'Login' is not static." I did not get that error when there were no ws-var in the template.

Changing it to

1
2
  let LoginWidget () = 
        LoginTemplate().Doc() 

gets rid of that error (but then I might as well remove the ws-templete="Login") no? Or am I misunderstanding something here. Still this is not my main issue.

I am using the login example page from the SPA example login app and applying that to my client server app. Unfortunately, this SPA example does not demo calling a server-side function.

My main issue at the moment, is though the click handler on the client side is called, (I see the console logs that I print) I cannot seem to get the server side method to be called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[<JavaScript>]
module Client =
...
    type LoginTemplate = Template<"Login.html">  // this gets subs into the main.html body.*

    let LoginWidget () = 
        LoginTemplate()
		  ...
            .Login(fun e ->
                passwordValid := not (String.IsNullOrWhiteSpace e.Vars.Password.Value)
                emailValid := not (String.IsNullOrWhiteSpace e.Vars.Email.Value)

                if passwordValid.Value && emailValid.Value 
                then 
                    Console.Log(sprintf "Your email is %s" e.Vars.Email.Value)  // I see this message in browse console.
                    Server.LoginClicked (e.Vars.Email.Value) |> ignore    // I have tried this 
                    async {  // i have tried and this 
                        (Server.LoginClicked e.Vars.Email.Value) |> ignore
                    } |> ignore

                else Console.Log("invalid input")

				e.Event.PreventDefault()

// * putting login html into main html

1
2
3
4
5
    let LoginPage ctx =
        Templating.Main ctx EndPoint.Login "Login" [
            h1 [] [text "Login"]
            div [] [client <@ Client.LoginWidget() @>]
        ]

and the server LoginClicked method looks like this (I have also added break points but they do not get triggered):

1
2
3
4
5
6
7
8
9
module Server =
...
    [<Rpc>]
    let LoginClicked (input: string) =
        File.WriteAllText("c:\\temp.txt", input)  // file does not get writen to
        Debug.Print("about to loggin: " + input)  // not sure where this 
        async {
            return input + " logged in"
        }

And I have tried qutie a few variants of that, but nothing happens, no file is written to, no break points trigger. etc. Basically, I cannot get the server side function to be called. I am clearly missing something.

Also the call to e.Event.PreventDefault() seems to stop the nice tooltip warning about the email missing @, if I remove that call I get the tool-tip.

How do I access the JS validation in the Click handler so I can check if that is valid or not before trying to call the server side function, that way I guess I can also decide not to call PreventDefault and thus still get the tool-tip message.

Oh! And what is the difference between .Doc() and .Bind()? I noticed in the SPA example it used .Bind(), but I get and error when I change Doc() to Bind() in my client-server app.

I have also tried code like this in my Login click handler (to try to mimic the SPA example):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	let Login = Var.Create ""   // Login is the name of the Button in the login.html file
        let submit = Submitter.CreateOption Login.View
        let vLogin =
            submit.View.MapAsync(function
                | None -> async { return "" }
                | Some input -> Server.LoginClicked input
            )

....
            .Login(fun e ->
...
                Console.Log("server click")
                (Server.LoginClicked e.Vars.Email.Value) |> ignore
                Console.Log("vLogin click")
                vLogin  |> ignore
                Console.Log("submit click")
                submit.Trigger |> ignore
                Console.Log("async submit click")
                async { submit.Trigger |> ignore } |> ignore
                Console.Log("check")
...
			)

and though all the console logs get displayed, none of the attempts to trigger the server side method are working.

I am admitably very new to F# and compeletely new to WebSharper, and I do thank you for your help so far.

What would be really handy is an example app that contained a bit more detail about the complete page life cycle. E.g. using templates (as this is better than embedded html), having fields and buttons on the page, "with the holes wired up," doing some client side checks on data before calling server method, submitting data to a server-side function (e.g. email and password), handling success and error case (error case being when function not successfully called, e.g. due to timeout so user can be notified of problem), displaying data returned from server side function in the browser, directing user to new page (in case of multiple page app), e.g. successful login page with simple welcome message "Hi <user> you are logged in."

This would give a more complete picture of the pluming required for a complete round trip.

At present it is really hard to find all this info and like I say I am having a real problem tring to pull bits together and get a really simple scenario to work.

I note the example Client Server app does show a server side method being called and a response to the client, but that is using in place HTML and not templates and though that bit is working in my sample app, I am having a real problem trying to work out what needs to change to get my .Login click handler to call the server side code.

Once again sorry for such a long message.

Cheers, Derek

By on 5/6/2018 9:38 PM ()

Hi Derek, I had a quick look and I think I know what you are missing. First, your RPC function is fine - Server.LoginClicked takes a string and returns Async<string>. However, the way you are trying to call it is not. Consider what you tried:

  1. Server.LoginClicked (e.Vars.Email.Value) |> ignore - This creates an Async value but throws it away immediately. 2. `fsharp async { (Server.LoginClicked e.Vars.Email.Value) |> ignore } |> ignore` This does the same, but asyncronously - so again, nothing is evaluated.

What you need instead is explicitly evaluating/starting the async function:

1
2
3
4
5
async {
    let! res = Server.LoginClicked e.Vars.Email.Value
    // do something with res
    return ()
} |> Async.StartImmediate

However, that's not all. The SPA project template (as opposed to the client-server one - websharper-web) by default is not set up to serve RPC functions, which is something we should really change and will do so in the next release. In order to enable this, add WebSharper.AspNetCore to your project references, open WebSharper.AspNetCore, and add this line to Startup.fs to prepare the server to respond to WebSharper RPC functions:

1
2
3
4
5
6
7
    member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) =
        ...
        app.UseDefaultFiles()
            .UseStaticFiles()
            .UseWebSharper(env)    // Add this line
            .Run(fun context ->
        ...

Hope this helps.

By on 5/8/2018 1:41 AM ()

Thanks Adam,

Changing the way the on LoginClicked method was called did the trick.

I am using the client-server app so I did not need the SPA config stuff. Though that is good to know.

Thanks again Derek

By on 5/8/2018 11:49 PM ()

Hi Adam, thanks for that.

I had actually looked at that example, but what I cannot figure out is if I have something like

1
2
type MainTemplate = Templating.Template<"Main.html">
type LoginTemplate = Template<"Login.html">

how do I insert the login template into the body of the main template?

So I need to be able to somehow get the "login" stuff into

1
2
3
4
5
6
7
8
let Main ctx action (title: string) (body: Doc list) =
    Content.Page(
        MainTemplate()
            .Title(title)
            .MenuBar(MenuBar ctx action)
            .Body(body)   // I assume I have to get login into here
            .Doc()
    )

In general I am hoping to be able to substitute different partial-templates into the body of main and keep each of the partial-templates in it's own file so that they can be managed separately.

Or are you saying that if I am to "embed" a partial-template into the body of main.html that i have to put the partial-templates into main.html as well, and subs that into body at runtime

In my login.html that I am trying to use I had wrapped it in

I am trying to use main.html like a "master page" so that it contains the boilerplate headers, nav, etc. and then depending on the requested endpoint subs the relevant partial temple as required into main's body.

also I have been reading https://developers.websharper.com/docs/v4.x/fs/ui and the section on Instantiating Templates makes mention of <ws-MyTpl Inner="Outer"> but then the example does not use that, I am not sure if this is relevant to what I am trying to do.

Sorry I am probably missing something obvious but as I say, I am very new to all this

Thanks again Derek

By on 5/5/2018 5:06 PM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper