/fQuery

F# Clone of Jquery

Primary LanguageF#

fQuery - jQuery for Fable

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.

Goals

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().

Current Implementation

This project is in its infancy, but it's probably more time consuming than particularly difficult.

Differences from jQuery

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.

Functions vs Objects

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.

Set and Get

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"

Examples

Selecting Elements.

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[]

Chaining with Pipes

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"

Element Manipulation

Just like you would in jQuery (that will be a recurring theme), except functions are called with pipe operators rather than on an object.

addClass (className: string) : fQuery

Adds a class to the selected elements

let links = f(%"a[href]") |> addClass "active"

removeClass (className: string) : fQuery

Removes a class from the selected elements

let links = f(%"a[href]") |> removeClass "active"

toggleClass (className: string) : fQuery

If an element already has a class, remove it. Else add it.

let links = f(%"a[href]") |> toggleClass "active"

attr (attribute: string) (value: string) : fQuery

Sets an attribute on selected elements.

let links = f(%"a[href]")
		|> attr "href" "https://github.com"

css (property: string) (value: string) : fQuery

Sets a CSS property of the selected elements

let links = f(%"a[href]")
		|> css "color" "red"
		|> css "background-color" "blue"

text (value: string) : fQuery

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"

getText : string

Retrieves the text value of the selected elements. Returns a string.
let heading = f(%"h1:first-of-type") |> getText

html (value: string) : fQuery

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>"

getHtml : string

Retrieves the inner html value of the selected elements. Returns a string.
let heading = f(%"h1:fi**rst-of-type") |> getHtml

is (selector: string) : boolean

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"

isFilter (selector: string) : fQuery

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"

Not yet implemented

I'm probably not aware of all of jQuery's functions, prop() is probably the largest and most important to implement.

Plucking from the fQuery collection

first : fQuery

Gives the first item in an fQuery collection

let firstParagraph = f(%"p") |> first

last : fQuery

Gives the last item in an fQuery collection

let lastParagraph = f(%"p") |> last

Event Handlers

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.

on (eventName: string) (selector: string or "") (callback: function Event -> unit) : fQuery

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)

Not Implemented yet

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.

Data

Setting data is done just like in Javascript.

data (key: string) (value: any) : fQuery

Attaches data to all of the selected elements. This data can be of any type.

f(%"body") |> data "person" {|name="Josh"|}

getData<type> (key: string) : Option<type>

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

Dom Traversal Functions

Lots to implement here, jQuery can do alot with the Dom!

find (selector: string) : fQuery

Starting from the selected elements, finds all descendants that match the given selector.
f(%"div")
    |> find "span.selected"

closest (selector: string) : fQuery

Starting from the selected elements, traverses upwards to find the first ancestor that matches the selector.
f(%"span.selected")
	|> closest "div"

parent (selector: string) : fQuery

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 ""

parent (selector: string) : fQuery

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 ""