I love F#, and I use it in all my personal apps, at least on the server side. I've never used Fable for the client though, because of external Javascript dependencies that would be too painful to write bindings for. The common denominator, both in my personal life and in the enterprise, is jQuery.
fQuery is a clone of jQuery, written in F#, and built for F# client-side apps using Fable.
fQuery takes a functional first approach to jQuery, with the eventual aim of implementing all functions that are in jQuery. It is aiming to provide the syntactical sugar that jQuery offers (combined with the syntactical sugar of F# pipes).
It is NOT intended to provide browser compatiblity in the way that jQuery originally was. I might also update some jQuery functionality to make it more consistent with modern Javascript standards. There will be no implementation of deprecrated jQuery functions like .click().
This project is in its infancy, but it's probably more time consuming than particularly difficult.
There are a few differences to jQuery - some are necessitated by the change of language, but others are just to make the most of modern Javascript. jQuery was made when Javascript was a very basic language - it didn't even have an array foreach method. So a lot of things that jQuery were made to solve, were solved in later versions of Javascript.
fQuery acts a relatively thin wrapper between F# and modern Javascript, while providing access to jQuery's helper functions.
In Javascript, jQuery return it's own type of object, that you can perform manipulations on.
In F#, fQuery provides a set of functions that return a collection of HTML elements. Each of these functions also take a collection of HTML elements as an argument, so you can almost always pass the return of one function into another, and chain functions together infinitely.
Since fQuery works on a collection of native HTML elements, you can also manipulate the elements directly with modern Javascript features, which gives it even more flexibility.
The other big difference between jQuery and fQuery is how values are setted and getted. In jQuery, you use the same function to set and get. E.g, in jQuery:
//Sets the id attribute of the first input on the page.
$('input').first().attr('id', 'myId')
//Returns the id attribute of the first input on the page.
$('input').first().attr('id')
In fQuery, set and get functions are different. This allows the set functions to be chainable.
The setter functions have the same function names as in jQuery, whereas the getter functions are prefaced with "get". For example data sets data on selected elements, whereas getData retrieves it. The same code as above, but in F# with fQuery:
//Sets the id attribute of the first input on the page.
f(%"input") |> first |> attr "id" "myId"
//Returns the id attribute of the first input on the page.
f(%"input") |> first |> getAttr "id"
In jQuery, you use the $ function to select elements. E.g $('div.className#id').
In fQuery, it's essentially identical, but you use the f function.
(* Returns all divs with the class "className" and the id "id". *)
f(%"div.className#id")
f() can take either a String, an Element or the document itself.
let fQueryDocument = f(%document)
let button = document.querySelector "button"
let fqueryButton = f(%button)
let buttons = f(%"button")
Are all perfectly valid and will return an fQuery type which can be passed to other fQuery functions.
At this stage, f() returns a collection of HTMLElements. Originally it returned a special union type of "fQuery". Since the item could be either a collection of elements or a Document. For now, I have a workaround that means I don't have to handle cases where document is the selected node. I'm not sure if it will hold when things get more complex but in the meantime the fQuery type is simply defined as an alias of HTMLElement[]
Pipes are the whole point of F# right?
All fQuery functions currently implemented both take and return a value of type fQuery (an alias of HTMLElement[]). So you can chain them infinitely
let paragraphs = f(%"p")
|> css "color" "blue"
|> css "width" "200px"
|> css "height" "200px"
|> last //Changes from here will only apply to the last paragraph
|> css "color" "red"
|> attr "id" "woot"
|> on "click" "" (fun e -> console.log "Hi" )
|> addClass "testClass"
|> removeClass "my-button"
Just like you would in jQuery (that will be a recurring theme), except functions are called with pipe operators rather than on an object.
Adds a class to the selected elements
let links = f(%"a[href]") |> addClass "active"
Removes a class from the selected elements
let links = f(%"a[href]") |> removeClass "active"
If an element already has a class, remove it. Else add it.
let links = f(%"a[href]") |> toggleClass "active"
Sets an attribute on selected elements.
let links = f(%"a[href]")
|> attr "href" "https://github.com"
Sets a CSS property of the selected elements
let links = f(%"a[href]")
|> css "color" "red"
|> css "background-color" "blue"
Sets the text value of selected elements. Unlike the jQuery function which is used to both get and set text, this function can only set text so that it can be used in pipes. To get the text from selected elements use getText instead.
let headings = f(%"h1")
|> css "font-size" "3em"
|> text "This is a heading"
|> addClass "heading"
Retrieves the text value of the selected elements. Returns a string.
let heading = f(%"h1:first-of-type") |> getText
Sets the inner HTML value of selected elements. Unlike the jQuery function which is used to both get and set html, this function can only set, so that it can be used in pipes. To get the inner html from selected elements use getHTML instead.
let headings = f(%"h1")
|> css "font-size" "3em"
|> html "<small>This is a heading</small>"
Retrieves the inner html value of the selected elements. Returns a string.
let heading = f(%"h1:fi**rst-of-type") |> getHtml
Checks to see if at least one of the selected elements matches the provided selector. is breaks the pipeline as it returns a boolean.
let paragraphs = $("%p") |> is "div"
Filters the selected elements to those that match the selector Unlike is, isFilter does not break the pipeline, so you can continue passing the result to other fQuery functions.
let paragraphs = $("%p") |> is ".className"
I'm probably not aware of all of jQuery's functions, prop() is probably the largest and most important to implement.
Gives the first item in an fQuery collection
let firstParagraph = f(%"p") |> first
Gives the last item in an fQuery collection
let lastParagraph = f(%"p") |> last
I love the way jQuery handles event handlers, and fQuery replicates that to the best of it's ability, providing near-identical syntax.
jQuery has an overloaded definition for it's event functions, fQuery does not. It uses a single function with 3 parameters. You can leave the 2nd parameter as an empty string if you don't need it, though I would recommend attaching all events to the body/document and using the second parameter.
Attaches an event to the selected elements. The first argument is the name of the event. It can also be a comma separated list of events. The second argument is a query selector. If it is left as an empty string, the event will be attached directly to selected element. If a selector is specified, the event will only be fired if an element matching the selector is the event target.
let docReady e = console.log e
f (%document) |> on "ready" "" docReady //fires when the document is ready
f (%"body")
|> on "click" "" (fun _ -> console.log("Body clicked")) //Fires whenever the body is clicked
|> on "click, mouseover" "button" (fun _ -> console.log("Just a button")) //Only fires if a button is clicked (or hovered over)
off() is a little harder to implement than I thought. I will have to see how jQuery handles it because the implementation I have is quite buggy.
Setting data is done just like in Javascript.
Attaches data to all of the selected elements. This data can be of any type.
f(%"body") |> data "person" {|name="Josh"|}
Returns an F# option type of the specified data. If the data has not been set previously, then fQuery will check the element to see if has a matching data attribute. The type must be explicitly stated.
let personData =
f(%"body")
|> data "person" {|name="Josh"|}
|> getData<{|name:string|}> "person"
console.log personData.Value
Lots to implement here, jQuery can do alot with the Dom!
Starting from the selected elements, finds all descendants that match the given selector.
f(%"div")
|> find "span.selected"
Starting from the selected elements, traverses upwards to find the first ancestor that matches the selector.
f(%"span.selected")
|> closest "div"
Selects the direct parents of the selected elements. Selector can be either an empty string "" or a query selector. If a selector is passed in, then if the parent does not match that selector it will be filtered out.
f(%"span.selected")|> parent ""
Selects all direct children of the selected elements. Selector can be either an empty string "" or a query selector. If a selector is passed in, then if the children do not match that selector, they will be filtered out.
f(%"span.selected")|> children ""