An embedding of Java Script into Curry
The development of Spicy Web should adhere to a list of destinations. By nature of these destinations, each change to the library will inhibit one of the destinations. The most important maxim is:
Do not cut back any of the destinations without an example relevant for practice!
Important: Finish one cycle before tackling the next one.
import Doc import Event import ElementAction main = writePage "toggle" (page "Toggle Demo" myDoc) myDoc = h1 (text "Toggle a paragraph with Javascript") <> (par (text "This paragraph can be enlarged and collapsed by clicking on \ \the extending link." <> toggle (text "This part of the text can be shown and hidden.")) `withStyle` "padding: 2em; width: 100px;") toggle helem = blank <> extension <> (more `onClick` display extension *> hide more *> display less) <> (less `onClick` hide extension *> display more *> hide less) where extension = hidden (helem <> blank) more = dlink (text "more") less = hidden (dlink (text "less"))
Initially, the second part of the text is hidden. When you click 'more', it is displayed and the 'more' link is replaced by a 'less' link.
import Doc import Event import ElementAction main = writePage "addStyle" (page "Change css class" doc) doc = heading `onFirstClick` addClassName heading "red" where heading = h1 (text "Make it red by clicking!")
If you click on the heading, it becomes red. Relies on a corresponding class defined in a separate css file.
import Doc import Event import ElementAction import EventAction import List ( intersperse ) main = writePage "hideClicked" (page "Hide Clicked" myDoc) myDoc = h1 (clickableWords "Click on a word to hide it!") <> par (clickableWords' "The same event-handler is used for every word") clickableWords :: String -> Doc clickableWords = foldr1 (<>) . intersperse blank . map (`onFirstClick`hideMe) . map text . words clickableWords' :: String -> Doc clickableWords' = foldr1 (<>) . intersperse blank . map (\d -> d `onFirstClick` hide d) . map text . words hideMe :: Action Doc hideMe = hideAction <*> (eventSourceAction <*> event)
This examples shows how to get information about the event within an action. There are two possibilities to hide a clicked word:
This example also demonstrates a problem: If the same sub-document is displayed more than once in the resulting HTML page, each occurrence is labeled by the same 'id' attribute. Here, the 'blank' element is duplicated by 'intersperse'.
import Doc import Event import ElementAction import EventAction main = writePage "observe" (page "Dynamically add event handlers" myDoc) myDoc = heading <> explanation `onFirstClick` observeOne heading "click" (hide explanation) where heading = h1 (text "Add an Event Handler by clicking!") explanation = par (text "Click on this paragraph to add an event handler to \ \the heading that hides this paragraph!")
Here, an event handler that hides the paragraph is added to the heading, when the paragraph is clicked.
There are two problems with the current approach:
Possible solution:
doc = heading `onFirstClick` addClassName (iD heading) "red" where heading = iDoc (h1 (text "Make it red by clicking!"))
Advantages:
Disadvantages:
To prevent sharing of documents that have an associated event handler or of documents that are accessed by event handlers restricts the expressivity of the programmer. Is it useful to share such documents? Might programmers want to do this? They might. Consider, for example, a link that shows you the “next page” in a large list of blog posts. Such links are usually placed at the top and the bottom of a page. Although there are multiple occurrences of the “next page” link, all occurrences share the same appearance and behavior. Hence, it is natural to define it once and share it in all places where it should be used. In all approaches discussed so far, sharing the “next page” link is only possible, if it does not have an associated event handler. There may be situations where it needs to have one.
As a conclusion, we consider another solution to the sharing problem that allows sharing of documents with references. As we cannot use the same 'id' attribute twice in an HTML document, we need to rename references that are used more than once. Taking this renaming into account, we can adapt the event handlers to operate also on the renamed ids. As a consequence, event handlers operate on lists of ids instead of single ids, so we can give this possibility to the programmer too.
The following example demonstrates how sharing of ids works. It uses a shared 'blank' that can be hidden by clicking on it. If one blank is clicked, all shared occurrences are also hidden.
main = makePage txt doc >>= writePage "hideBlanks" txt = "Hide all blanks by clicking on one!" doc = h1 (foldr1 (<>) (intersperse vanishingBlank (map text (words txt)))) where vanishingBlank = blank `onClick` hide [iD vanishingBlank]
There are some things worth noting:
If every document has an ID, then the generated HTML page is cluttered with id attributes that are never used. Fortunately, we know which IDs are used and can eliminate the others before creating the page. But we can do even more: We can throw an error, when the programmer uses IDs of documents that are not used in the generated page.
main = makePage "error page" doc >>= writePage "unused" where doc = text "hello" `onClick` hide [iD (text "world!")]
In the above example, the user will get an error when trying to create the page, because the ID of the hidden document is not part of the page.
That said, having explicit IDs in the interface to the programmer becomes questionable. They do not prevent errors and they do not allow more powerful error detection. We can go back to the old interface with “documents only” and are still able to detect misplaced documents in event handlers. Reconsider the toggle example - this time in two versions:
Explicit IDs:
toggle helem = blank <> extension <> (more `onClick` display extID *> hide moreID *> display lessID) <> (less `onClick` hide extID *> display moreID *> hide lessID) where extension = hidden (helem <> blank) extID = [iD extension] more = dlink (text "more") moreID = [iD more] less = hidden (dlink (text "less")) lessID = [iD less]
Documents only:
toggle helem = blank <> extension <> (more `onClick` display extension *> hide more *> display less) <> (less `onClick` hide extension *> display more *> hide less) where extension = hidden (helem <> blank) more = dlink (text "more") less = hidden (dlink (text "less"))
Which do you prefer?
To improve our library we could use type information. For example we enrich documents by a phantom type and can ensure that event handlers are attached to the correct documents. For example the onClick event handler can be attached to any document while the onChange handler can only be attached to input, select and textarea elements.
For example the onClick event handler takes any type of document which is represented by the polymorphic variable a.
onClick :: Doc a -> Action _ -> Doc a
The event handler onChange can only be attached to input, select and textarea elements. This group of elements is represented by the type InputAndSelect.
onChange :: Doc (InputAndSelect a) -> Action _ -> Doc (InputAndSelect a)
The type information restricts to set of valid programs. Thus we have to check whether we can live with these restrictions. For example one restriction is that we can only put documents of the same type into a list.
The operation <> takes two documents and combines them. The resulting document is not a valid document in the sense that we cannot add an event handler to it. Therefore we use the Type None for the result of this operation.
(<>) :: Doc _ -> Doc _ -> Doc None
Let's take a look at clickableWords'. The documents text and blank are Text documents.
text :: String -> Doc Text blank :: Doc Text
Therefore the definition of clickableWords' causes a type error. The function foldr1 has type (a → a → a) → [a] → a. This is not true in this application because the list has type [Doc Text] while <> yields type Doc None.
clickableWords' = foldr1 (<>) . intersperse blank . map (\d -> d `onFirstClick` hide d) . map text . words
More coming soon …