Login in a multi-page website (SAFE Stack) - f#

Does anybody use the Elmish routing for a multi-page app (SAFE Stack in my case) as specified by Maxime Mangel here on StackOverflow , but with a login page?
Do you, at the same time, use the accessibility (Anonymous/LoggedIn) philosophy as specified here in the Elmish Book?
If so, you probably use different coding than in the Elmish Book (as I do), but in any case you probably need to have login results available in the main page (Main.fs) to implement the accessibility logic. The Elmish Book uses (in Login.fs) a function with an active pattern let (|UserLoggedIn|_|) = ... and this function is called from the main or home page (see here).
But it wouldn't work in my app.
So my question is:
How do you transfer login results from the login page (Login.fs) into the main page (Main.fs)?
You can easily spot my login results here in the code:
Login.fs //client
let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
match msg with
| SetUsrInput value -> { model with InputUsr = value }, Cmd.none
| SetPswInput value -> { model with InputPsw = value }, Cmd.none
| SendUsrPswToServer ->
let buttonClickEvent = SharedLoginValues.create model.InputUsr model.InputPsw
let cmd = Cmd.OfAsync.perform getLoginApi.login buttonClickEvent GetLoginResults
model, cmd
| GetLoginResults value ->
let result =
match value with
| SharedApi.UsernameOrPasswordIncorrect -> { model with User = ApplicationUser.Anonymous}
| SharedApi.LoggedIn user -> { model with User = ApplicationUser.LoggedIn user}
result, Cmd.ofMsg AskServerForSecurityTokenFile
Meanwhile, I use a workaround with reaching login results that I save on the server. Although the accesibility logic works as expected, the workaround seems to be cumbersome - I already have the login results in the login page so why reaching them twice...
Here, it is my code for the accesibility logic in Main.fs that should use the login results. By the way, the code is, to my big surprise, much simpler than that in the Elmish Book.
Main.fs //client
let private setRoute (optRoute: RouterM.Route option) model =
let model =
let applicationUser =
//model.GetSecurityTokenFile -> this is the workaround
// - results from the login page should be matched here instead
match model.GetSecurityTokenFile with
| true -> LoggedIn model.user
| false -> Anonymous
let currentRoute =
//model.GetSecurityTokenFile -> this is the workaround
// - results from the login page should be matched here instead
match model.GetSecurityTokenFile with
| true -> optRoute
| false -> Some RouterM.Route.Home //or to a login page
{
model with CurrentRoute = currentRoute
User = applicationUser
}
Well, just 2x match and that's really all. Or have I missed anything here?
match optRoute with
//...some code
| Some (RouterM.Route.CMSRozcestnik cmsRozcestnikId) ->
match model.User with
| Anonymous ->
let (homeModel, homeCmd) = Home.init () //or Login.init
{ model with ActivePage = Page.Home homeModel }, cmd2 HomeMsg homeCmd AskServerForDeletingSecurityTokenFile
| LoggedIn user ->
let (cmsRozcestnikModel, cmsRozcestnikCmd) = CMSRozcestnik.init cmsRozcestnikId
{ model with ActivePage = Page.CMSRozcestnik cmsRozcestnikModel }, Cmd.map CMSRozcestnikMsg cmsRozcestnikCmd
| _ -> let (homeModel, homeCmd) = Home.init () //or Login.init
{ model with ActivePage = Page.Home homeModel }, cmd2 HomeMsg homeCmd AskServerForDeletingSecurityTokenFile
If needed, the entire code is here on GitHub:
https://github.com/MiroslavHustak/SAFE-Stack-simple-multipage-website-with-CMS-

I have received an answer to my question from Maxime Mangel via F# Slack.
Look at the section "Make the child communicate with the parent" in the link below and pay attention to the message ExternalMsg. Then just implement the code into your system. It worked with my code. If interested, you can look at the code on GitHub (the link is in the question).
https://medium.com/#MangelMaxime/my-tips-for-working-with-elmish-ab8d193d52fd

Related

Why cyrsasl_scram mechanism is not allowing base64 GUID?

I am writing a chat communication app.
If the user's unique id is given in Base 64 GUID format, it is throwing bad_username error.
In this file: https://pow.gs/mirror/ejabberd/-/blob/fd8e07af4789be362a61755ea47f216baeb64989/src/cyrsasl_scram.erl, there is a method to remove the == from username:
unescape_username(<<"">>) -> <<"">>;
unescape_username(EscapedUsername) ->
Pos = str:str(EscapedUsername, <<"=">>),
if Pos == 0 -> EscapedUsername;
true ->
Start = str:substr(EscapedUsername, 1, Pos - 1),
End = str:substr(EscapedUsername, Pos),
EndLen = byte_size(End),
if EndLen < 3 -> error;
true ->
case str:substr(End, 1, 3) of
<<"=2C">> ->
<<Start/binary, ",",
(unescape_username(str:substr(End, 4)))/binary>>;
<<"=3D">> ->
<<Start/binary, "=",
(unescape_username(str:substr(End, 4)))/binary>>;
_Else -> error
end
end
end.
I don't know why this has been written. If I remove this particular code, the connection is working fine. Please let me know why it is restricted.
If the users' unique id is given in Base 64 GUID format, it is throwing bad_username error.
Right:
call xmpp_sasl_scram:unescape_username(<<"user1">>)
returned from xmpp_sasl_scram:unescape_username/1 -> <<"user1">>
call xmpp_sasl_scram:unescape_username(<<"user3==ABC">>)
returned from xmpp_sasl_scram:unescape_username/1 -> error
call xmpp_sasl_scram:unescape_username(<<"user4=DEF">>)
returned from xmpp_sasl_scram:unescape_username/1 -> error
call xmpp_sasl_scram:unescape_username(<<"user5=">>)
returned from xmpp_sasl_scram:unescape_username/1 -> error
I don't know why this has been written. If I remove this particular code, the connection is working fine. Please let me know why it is restricted.
I don't know, either. But that code exists since nine years ago:
https://github.com/processone/ejabberd/commit/e80b92b48148505b44c6a378db36badfe60fce79#diff-5c51943c1268ffe26fe3b041b20675c6R136
Whatever reason there is for it, it's obviously a good reason.

box callback functions returning the same string in Rascal

I'm trying to draw some boxes in Rascal and trying to give each box its own callback function. On entering the box with the mouse the corresponding string should get displayed in the text element (so hovering box1 should display box1 etc.).
However, at the moment the text does pop up but just displays "box3" for each of the 3 boxes.
Any ideas?
strings = ["box1", "box2", "box3"];
boxes = [ box(
size(100, 100),
onMouseEnter(void() {
output = s;
})
) | s <- strings];
render(hcat([
vcat(boxes),
text(str () {return output;})
]));
Good question, classical problem. The essence of the problem is that Rascal uses "non-capturing closures": this means that functions that are returned from another function share the same context. In your case this is the variable s introduced by s <- strings. This nearly always happens when you create function values in a loop (as you do here). The solution is to wrap another function layer around the returned function.
Here is a simple example:
list[int()] makeClosures()
= [ int() {return i;} | i <- [0,1,2]];
void wrong(){
lst = makeClosures();
println(lst[0]());
println(lst[1]());
println(lst[2]());
}
which will print surprisingly the values 2,2and2`. The solution is, as said, to introduce another function level:
int() makeClosure(int i)
= int() { return i;};
list[int()] makeClosuresOK()
= [ makeClosure(i) | i <- [0,1,2]];
void right(){
lst = makeClosuresOK();
println(lst[0]());
println(lst[1]());
println(lst[2]());
}
now calling right() will print 1, 2, and 3 as expected.
I leave it as an exercise how this is done in your example, but I am prepared to give a solution when you ask for it. Good luck!

Elm - textarea selection range disappearing

I implemented a <textarea> in Elm such that tabs indent and unindent instead of change focus to another HTML element. Works great except that unindenting sometimes causes the selection to disappear! If I'm selecting the 5th character to the 12th character, I press shift-tab, then it removes 2 tab characters, but it also makes the selection change to a cursor at position 10. The selection range should remain the same..
I have an SSCCE at Ellie: https://ellie-app.com/3x2qQdLqpHga1/2
Here are some screenshots to illustrate the problem. Pressing Setup shows this:
Then pressing Unindent should show the following (with the selection of "def\ng" still intact):
Unfortunately, pressing Unindent actually shows the following. The text is unindented fine, but the selection range goes away and there's just a cursor between the g and the h:
Interesting issue and excellent problem illustration!
The problem is that for some reason re-rendering doesn't occur when one of the selectionStart/selectionEnd properties remains the same. Try changing 5 to 6 on line #42.
It works when you introduce a forced reflow in the element structure. See here: https://ellie-app.com/6Q7h7Lm9XRya1 (I updated it to 0.19 to see if that would solve the problem, but it didn't).
Note that this probably re-renders the whole textarea anew so it might cause problems if the textarea is a huge piece of code. You could solve that by alternating between two identical textareas where you toggle their visibility every render.
module Main exposing (Model, Msg(..), main, update, view)
-- Note: this is Elm 0.19
import Browser
import Browser.Dom exposing (focus)
import Html exposing (Html, button, div, text, textarea)
import Html.Attributes exposing (attribute, class, cols, id, property, rows, style, value)
import Html.Events exposing (onClick)
import Html.Lazy exposing (lazy2)
import Json.Encode as Encode
import Task exposing (attempt)
type alias Model =
{ content : String
, selectionStart : Int
, selectionEnd : Int
-- keep counter of renderings for purposes of randomness in rendering loop
, renderCounter : Int
}
main =
Browser.element
{ init = initModel
, view = view
, update = update
, subscriptions = \s -> Sub.none
}
initModel : () -> ( Model, Cmd Msg )
initModel flags =
( Model "" 0 0 0, Cmd.batch [] )
type Msg
= Setup
| Unindent
| NoOp (Result Browser.Dom.Error ())
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
newRenderCounter =
model.renderCounter + 1
in
case msg of
Setup ->
( { model
| content = "\tabc\n\tdef\n\tghi"
, selectionStart = 5
, selectionEnd = 12
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
Unindent ->
( { model
| content = "\tabc\ndef\nghi"
, selectionStart = 5
, selectionEnd = 10
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
NoOp _ ->
( model, Cmd.batch [] )
view : Model -> Html Msg
view model =
div []
(viewTextarea model model.renderCounter
++ [ button [ onClick Setup ] [ text "Setup" ]
, button [ onClick Unindent ] [ text "Unindent" ]
]
)
viewTextarea : Model -> Int -> List (Html msg)
viewTextarea model counter =
let
rerenderForcer =
div [attribute "style" "display: none;"] []
ta =
textarea
[ id "ta"
, cols 40
, rows 20
, value model.content
, property "selectionStart" <| Encode.int model.selectionStart
, property "selectionEnd" <| Encode.int model.selectionEnd
]
[]
in
-- this is the clue. by alternating this every render, it seems to force Elm to render the textarea anew, fixing the issue. Probably not very performant though. For a performant version, use an identical textarea instead of the div and make sure the two selectionStart/end properties both differ from the previous iteration. Then alternate visibility between the two every iteration.
if isEven counter then
[ ta, rerenderForcer ]
else
[ rerenderForcer, ta ]
isEven : Int -> Bool
isEven i =
modBy 2 i == 0

Refactoring coffee script method

I have put this method together to achieve some basic validation in a rails app.
I'm very new to rails/coffeescript and wondering if anyone has ideas on refactoring/simplifying it:
validateBillingAddress: (event) ->
add1 = $('#user_billing_address_1')
city = $('#user_billing_city')
zip = $('#user_billing_zip')
add1.removeClass('error')
city.removeClass('error')
zip.removeClass('error')
if !$('#user_billing_agreement').is(':checked')
$('button[type=submit]').removeAttr('disabled')
alert('You must agree to the subscription')
return
if !add1.val().length
$('button[type=submit]').removeAttr('disabled')
add1.addClass('error')
return
else if !city.val().length
$('button[type=submit]').removeAttr('disabled')
city.addClass('error')
return
else if !zip.val().length
$('button[type=submit]').removeAttr('disabled')
zip.addClass('error')
return
else
#processCard()
event.preventDefault()
I think you could try something like this (not tested)
validateBillingAddress: (event) ->
event.preventDefault()
fields = $('#user_billing_address_1, #user_billing_city, #user_billing_zip')
fields.removeClass('error')
unless $('#user_billing_agreement').is(':checked')
$('button[type=submit]').removeAttr('disabled')
alert('You must agree to the subscription')
return
fields.each ->
if !$(this).val().length
$('button[type=submit]').removeAttr('disabled')
$(this).addClass('error')
if fields.filter('.error').length > 0
return
else
#processCard()
Here is my take on it.
Very small, single-concern functions. This might be overkill, but I've found that most applications will grow in complexity, and getting a clean structure early helps later.
Also it sets the stage for reusing this code later on other forms. Perhaps even making a coffeescript class.
#
# Handle submit event, validate, and submit
#
$('form').on "submit", ->
if isValidBillingAddress() and isAgreementChecked()
#processCard()
#
# Validation checks
#
isAgreementChecked = ->
if $('#user_billing_agreement').is(':checked')
true
else
alert('You must agree to the subscription')
false
isValidBillingAddress = ->
enableForm()
for field in requiredFormFields()
do (field) ->
if isInvalidData(field)
showErrorOnField(field)
else
field.removeClass('error')
#
# Support functions
#
requiredFormFields = ->
address1 = $("#user_billing_address_1")
city = $("#user_billing_city")
zip = $("#user_billing_zip")
[address1, city, zip]
isInvalidData = (field) ->
field.val() is ""
showErrorOnField = (field) ->
field.addClass('error')
enableForm = ->
$('button[type=submit]').removeAttr('disabled')

F# Silverlight RPC: pre-fill paginated data

Thanks to everybody who has helped over the past few months trying to help me get my silverlight / f# prototype up and running (started in the RC version of VS - Ugh). The last problem we are trying to solve is an RPC issue.
We need to have the ability to paginate RPC calls, such that the first page is requested and bound to the grid and displayed, while the otehr pages are prefilled in the background and concatenated together. I guess psuedo code would look like this:
let pageNo = 1
let page1Data = JsonRpc.getSomeData(pageNo)
let grid.datasource <- page1Data
let grid.suspendFiltering <- true
// run the remainder in background
let allData : list ref = page1Data ref
for pageNo in [2..totalPages]
allData := allData # JsonRpc.getSomeData(pageNo)
let grid.datasource <- allData
let grid.suspendFiltering <- true
I appologize for the code above, I tried to make it as F# like as possible (writing in this text window); another flaw is the need to use call backs to bind the data to grids etc.
The question approaches might be used to solve this problem and what is the most approriate?
hmm... something like this? (typing in browser so may contain errors):
module Loader
open System
open System.Threading
let totalPages = 20
// emulation of long-running data loading routine
let private loadPageData (page : int) =
async {
do! Async.Sleep(1000)
return List.replicate 5 page
}
// loader - notifies UI about new data via callback
let loadAsync (callback : System.Action<_>) =
let syncContext = SynchronizationContext.Current
let doLoad = async {
// load first page and immediately feed it to callback
let! page1Data = loadPageData 1
do! Async.SwitchToContext syncContext
callback.Invoke(ResizeArray<_>(page1Data))
// load remaining data in the background
do! Async.SwitchToThreadPool()
let allData = ResizeArray<_>(page1Data)
for page in 2..totalPages do
let! pageData = loadPageData page
allData.AddRange(pageData)
do! Async.SwitchToContext syncContext
callback.Invoke(allData)
}
Async.Start doLoad
On UI side it will appear like this (i.e. data - ListBox or some other control)
Loader.loadDataAsync(list => data.ItemSource = list)

Resources