Sort, drag and drop in UI Next with Sortable
Sort, drag and drop in UI Next with Sortable
Few weeks ago I covered how to use external JS libraries with WebSharper.
I explained how we could integrate tagsinput
which is a cool library that allows us to use tags in our webapp.
It was used with JQuery and I showed you how we could extended WebSharper JQuery and add tagsinput
functionalities.
Today I will show you how to use another cool JS library - Sortable. Sortable brings drag - drop - sorting functonalities. Also, it does not require JQuery which is good as we can see how to integrate libraries which don’t depend on JQuery.
Here is a preview of what we will be building:
You can find the full source code here.
How does Sortable works in JS?
Sortable examples can be found here. On top of allowing us to sort elements, it also provides drag and drop functionalities which are very handy to make interactive webapps.
In JS, all you need to do is to create a list of elements (ul
, ol
or div
containing other elements) and pass it to the create
function of Sortable
.
Sortable.create(myelement, { .. some options ... })
And that’s it. myelement
is now a sortable list. Let’s see how can we use that in WebSharper
.
Create a link from F# to Sortable
As we saw earlier, the main function to call is Sortable.create
.
It takes an element and some options as parameter.
Elements in WebSharper
are translated with the type Dom.Element
.
The options
will be held in a record type.
We can now directly create a link:
[<JavaScript>]
module Sortable =
[<Direct "Sortable.create($el, $options)">]
let sortableJS (el: Dom.Element) options = X<unit>
We can then call sortableJS
to make an element sortable.
divAttr [ on.afterRender(fun el -> sortableJS el Unchecked.defaultof<_>) ]
[ div [ text "Aa" ]
div [ text "Bb" ]
div [ text "Cc" ]
div [ text "Dd" ]
div [ text "Ee" ] ]
We need to place the call to sortableJS
in on.afterRender
because the dom needs to be created before we call Sortable.create
.
The div
is now completely sortable and draggable. We can move Aa
or Bb
around.
But what about the options?
If we just need to give the ability to sort a list, we would be done. But chances are that we need to do more, like do an action after sorting the list or drag and dropping into another list. And as we saw in the preview, we will be making drag and drop in between
Link Sortable options
Sortable has many options
and you can find most of them in the readme,
We will see how we can bind few options
and from there you will be able to apply the same method to use other functionalities.
The functionalities we are interested in are: - Group - Sort - Animation - OnXXX (OnAdd, OnSort, etc…)
Group
We need to identify our lists.
We will name the droppable list Workspace
and the drag and drop lists ListA
and ListB
:
Workspace
will be a place to drop item in.ListA
will be a place to drag items from to drop intoWorkspace
.ListB
will be a place to clone items from and drop intoWorkspace
.
Sortable
has a first member called group
.
A group
is defined by a name
and a pull
action and put
action.
pull
defines the behaviour of pulling item from the list (drag) and put defines the behaviour of putting item into another list (drop).
Here is the implementation of group
:
type Group = {
[<Name "name">]
Name: string
[<Name "pull">]
Pull: string
[<Name "put">]
Put: string
}
with
static member Create name pull put =
{ Name = name
Pull = pull |> Pull.ConvertToJSOption
Put = put |> Put.ConvertToJSOption }
and Pull =
| Allow
| Disallow
| Clone
with
static member ConvertToJSOption =
function
| Allow -> "true"
| Disallow -> "false"
| Clone -> "clone"
and Put =
| Allow
| Disallow
| AllowList of string list
with
static member ConvertToJSOption =
function
| Put.Allow -> "true"
| Put.Disallow -> "false"
| Put.AllowList list -> list |> (String.concat "," >> sprintf "[%s]")
We need to use Name
attribute because JS is case sensitive so we need to define the binding ourself if we capitalize our members.
Also note that I am using string
for Pull
and Put
in order to respect the type expected by Sortable
.
We can now create group
simply by doing:
Group.Create "Workspace" Pull.Disallow <| Put.AllowList [ "ListA"; "ListB" ]
Sort and animation
Next we need to configure whether the list is sortable or not.
This is done with Sort
.
It is useful when you want to restrict the list to only be draggable and droppable but not sortable.
Animation
just specify the duration of the drag and drop animation and sort animation.
So if we create our option
record type, it would be:
type Sortable = {
[<Name "group">]
Group: Group
[<Name "sort">]
Sort: bool
[<Name "animation">]
Animation: int
}
with
static member Default =
{ Group = Group.Create "" Pull.Allow Put.Allow
Sort = true
Animation = 150 }
static member SetGroup group (x: Sortable) =
{ x with Group = group }
static member AllowSort (x: Sortable) =
{ x with Sort = true }
static member DisallowSort (x: Sortable) =
{ x with Sort = false }
static member Create el (x: Sortable) =
sortableJS el x
and Group = {
[<Name "name">]
Name: string
[<Name "pull">]
Pull: string
[<Name "put">]
Put: string
}
with
static member Create name pull put =
{ Name = name
Pull = pull |> Pull.ConvertToJSOption
Put = put |> Put.ConvertToJSOption }
and Pull =
| Allow
| Disallow
| Clone
with
static member ConvertToJSOption =
function
| Allow -> "true"
| Disallow -> "false"
| Clone -> "clone"
and Put =
| Allow
| Disallow
| AllowList of string list
with
static member ConvertToJSOption =
function
| Put.Allow -> "true"
| Put.Disallow -> "false"
| Put.AllowList list -> list |> (String.concat "," >> sprintf "[%s]")
We can then use this Sortable
in an on.afterRender
like so:
divAttr [ on.afterRender(fun el ->
Sortable.Default
|> Sortable.AllowSort
|> Sortable.SetGroup (Group.Create "listA" Pull.Allow Put.Disallow)
|> Sortable.Create el) ]
[ div [ text "Aa" ]
div [ text "Bb" ]
div [ text "Cc" ]
div [ text "Dd" ]
div [ text "Ee" ]
Next what we want is to handle all events when items are dropped or when items are sorted.
Handling events
Specifying callbacks to be called when events happen is done from the options
as well.
We can bind callbacks like onAdd
, onSort
, onUpdate
from the options
.
Every callback takes an event
as parameter.
So each callback has a type of Event -> unit
.
The event
parameter contains properties which are helpful to manage our lists.
Here’s the defnition of the Event
:
SortableEvent = {
[<Name "item">]
Item: Dom.Element
[<Name "from">]
From: Dom.Element
[<Name "to">]
To: Dom.Element
[<Name "newIndex">]
NewIndex: int
[<Name "oldIndex">]
OldIndex: int
}
It’s straight forward, Item
is the item being dropped, From
is the list from where the item is from, To
is the list destination, NewIndex
is the index at which the item is dropped at and OldIndex
was the index at which the item from dragged out from.
Using this we can now define OnAdd
:
[<Name "onAdd">]
OnAdd: SortableEvent-> unit
And we can also add a helper method on Sortable
:
static member SetOnAdd onAdd (x: Sortable) =
{ x with OnAdd = onAdd }
With that we will be able to configure our Sortable
record.
And we are now done!
Use Sortable
We can now create our three lists and it should work the same way as the preview!
[<JavaScript>]
module Client =
open Sortable
let panel title body =
divAttr [ attr.``class`` "panel panel-default" ]
[ divAttr [ attr.``class`` "panel-heading" ] [ text title ]
divAttr [ attr.``class`` "panel-body" ] [ body] ]
let main() =
divAttr [ attr.``class`` "row" ]
[ divAttr [ attr.``class`` "col-sm-4" ]
[ panel
"Workspace: droppable from ListA and ListB"
(divAttr [ attr.style "min-height:100px;"
on.afterRender(fun el ->
Sortable.Default
|> Sortable.SetGroup (Group.Create "workspace" Pull.Allow <| Put.AllowList [ "listA"; "listB" ])
|> Sortable.Create el) ]
[]) ]
divAttr [ attr.``class`` "col-sm-4" ]
[ panel
"ListA: draggable and sortable"
(divAttr [ on.afterRender(fun el ->
Sortable.Default
|> Sortable.AllowSort
|> Sortable.SetGroup (Group.Create "listA" Pull.Allow Put.Disallow)
|> Sortable.Create el) ]
[ div [ text "Aa" ]
div [ text "Bb" ]
div [ text "Cc" ]
div [ text "Dd" ]
div [ text "Ee" ] ]) ]
divAttr [ attr.``class`` "col-sm-4" ]
[ panel
"ListB: draggable and cloned"
(ulAttr [ on.afterRender(fun el ->
Sortable.Default
|> Sortable.DisallowSort
|> Sortable.SetGroup (Group.Create "listB" Pull.Clone Put.Disallow)
|> Sortable.Create el) ]
[ li [ text "11" ]
li [ text "22" ]
li [ text "33" ]
li [ text "44" ]
li [ text "55" ] ]) ] ]
You can find the full source code here.
Conclusion
And that’s it! That is all we need to use Sortable
https://github.com/RubaXa/Sortable, an amazing JS library, with WebSharper in F#.
Today we saw how to bind JS libraries. Also we saw how we could directly use our own record types to pass it to JS functions and finally we saw that record types could be used directly to deal with results of JS functions.
As always if you have any comments please leave it below or hit me on Twitter @Kimserey_Lam. Thanks for reading!
Comments
Post a Comment