Table of Contents
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!
- As well typed as possible on the Curry side.
- As few Curry primitives as possible.
- As few Java Script primitives as possible, implemented in a maintainable way.
- Primitives as expressive as possible.
- Easy to program with, flat learning curve.
- Efficiency on the Java Script side.
- Efficiency on the Curry side.
- Independent of browser implementations.
The Working Cycle
- Choose an example relevant for practice
- Document example with code design
- Design primitives needed to implement example
- Implement primitives on Curry side
- Test example implementation in different browsers.
- Revise the concept, refactor and complete documentation.
Important: Finish one cycle before tackling the next one.
Hiding and displaying elements
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.
Changing the css class of an element
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.
Hiding words by clicking on them
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:
- explicitly pass the word to hide to the event handler so that it can hide the word
- ask for the 'event source' in the event handler to get the clicked element
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'.
Dynamically associating event handlers
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:
- no separation between documents and their reference that is used in event handlers
- sharing documents leads to duplicated ids in the HTML document
- hidden reference creation
- distinguishing documents with and without references
- event handler take references instead of documents
doc = heading `onFirstClick` addClassName (iD heading) "red" where heading = iDoc (h1 (text "Make it red by clicking!"))
- can share documents without duplicating ids
- no newly created documents in event handlers
- still cannot share documents with ids
- additional concept of references
- you can still use references of documents that are not used in the HTML page
Is Sharing Useful?
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:
- Event handlers operate on multiple ids simultaneously.
- There is no explicit creation of references. As sharing of references is now possible, there is no need for documents without references.
- The function creation of the page is now an IO action as it involves an unsafe test on ids to check whether they are bound.
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:
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]
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 …