1
Fork 0

Add a new documentation site to the repository ()

* Update config

* Update readme.

* Update readme.md

* Add getting started section to tutorials.

* Added getting-started with updates.

* Major update to getting started guide

* Overhaul to tutorials.
This commit is contained in:
Thomas Honeyman 2018-05-23 19:41:37 -07:00 committed by GitHub
commit 0721d52d04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1465 additions and 392 deletions

View file

@ -38,45 +38,53 @@ jobs:
paths:
- ~/select/node_modules
# Verify all examples successfully build...
- run:
name: Build all components via docs
command: yarn run build-docs
name: Build example components
command: |
yarn build-docs
# Persist the specified paths into the workspace
- persist_to_workspace:
root: docs/dist
root: docs
paths:
- .
- js
site:
working_directory: ~/select
docker:
- image: rustlang/rust:nightly
css:
<<: *defaults
steps:
- checkout
- run:
name: Install Gutenberg
name: Fetch and persist CSS
command: |
wget https://github.com/Keats/gutenberg/releases/download/v0.3.1/gutenberg-v0.3.1-x86_64-unknown-linux-gnu.tar.gz -O gb.tar.gz
tar -xvzf gb.tar.gz
yarn fetch-css && yarn move-css
- attach_workspace:
at: docs/dist
- persist_to_workspace:
root: docs
paths:
- css
site:
working_directory: ~/select
docker:
- image: jfloff/alpine-python:3.4-slim
steps:
- checkout
- setup_remote_docker
- run:
name: Build site
command: |
mv gutenberg docs/site
cd docs/site
./gutenberg build
cd ../
cp -r site/public/. dist/
name: Install dependencies
command: pip install mkdocs mkdocs-material
- attach_workspace:
at: docs
- run:
name: Run site generation
command: mkdocs build
# Persist the dist folder
- persist_to_workspace:
root: docs/dist
root: site
paths:
- .
@ -98,7 +106,7 @@ jobs:
git worktree add ../gh-pages gh-pages
- attach_workspace:
at: dist
at: site
# Enable building docs
- add_ssh_keys:
@ -108,14 +116,14 @@ jobs:
- run:
name: Copy over files to gh-pages branch
command: |
cp -r dist/. ../gh-pages
cp -r site/. ../gh-pages
cp -r .circleci/. ../gh-pages/.circleci
- run:
name: Push update to GitHub
command: |
cd ../gh-pages
git config --global user.email "dev@citizennet.com"
git config --global user.email "admin@thomashoneyman.com"
git config --global user.name "CircleCI Build"
git add .
git commit --allow-empty -m "Build triggered by CircleCI"
@ -131,10 +139,22 @@ workflows:
ignore: gh-pages
# On master branch, rebuild docs
- css:
filters:
branches:
only:
- master
- docs
requires:
- test
- site:
filters:
branches:
only: master
only:
- master
- docs
requires:
- test
@ -142,7 +162,10 @@ workflows:
- docs:
filters:
branches:
only: master
only:
- master
- docs
requires:
- site
- css

5
.gitignore vendored
View file

@ -11,8 +11,9 @@ package-lock.json
generated-docs
# Dist
docs/dist
docs/site/public
docs/css
docs/js
site
# IDE
*.swo

View file

@ -0,0 +1,84 @@
# Understanding Free Monad Queries
Most of the time Halogen queries look like this:
```hs
data QueryF (… other type arguments omitted …) a
= ...
| SetVisibility Visibility a
| GetVisibility (Visibility -> a)
```
(where `#!hs QueryF` is used directly as the Halogen query functor)
This library takes a slightly different approach: the query functor is actually `#!hs Control.Monad.Free.Free QueryF`, the [free monad](https://pursuit.purescript.org/packages/purescript-free/4.2.0/docs/Control.Monad.Free) generated by the query functor.
This allows queries the full power of monadic (and applicative) composition: sequencing effects, determining control flow based on previous results, and my favorite: doing nothing (`#!hs pure unit`).
We now define smart query constructors for this Free pattern like so:
```hs
-- | Set the container visibility (`On` or `Off`).
setVisibility :: ∀ o item eff. Visibility -> Query o item eff Unit
setVisibility v = liftF (SetVisibility v unit)
-- | Get the container visibility (`On` or `Off`). Most useful when sequenced
-- | with other actions.
getVisibility :: ∀ o item eff. Query o item eff Visibility
getVisibility = liftF (GetVisibility id)
```
## Different patterns
In the simple cases, the helpers Halogen use to work with raw query constructors are folded into the smart Free query constructors already: `#!hs H.action (SetVisibility On)` becomes simply `#!hs setVisiblity On`, and similarly `#!hs H.request GetVisibility` is just `#!hs getVisibility`. This is because these patterns are typically present already smart constructors: `#!hs setVisibility` returns `#!hs Free QueryF Unit`, since it is an action, and `#!hs getVisibility` returns `#!hs Free QueryF Visibility`, since it requests the visibility. This allows for easy composition in `#!hs do` notation:
```hs
toggleVisibility = do
vis <- getVisibility
setVisibility (not vis)
```
Cest très facile!
Event handlers look a little different. This is one example:
```hs
HE.onMouseDown \ev -> Just do
Select.preventClick ev
Select.select index
when doBlur Select.triggerBlur
```
(Of course you may return `#!hs Nothing` if you so wish, but its effect is just like `#!hs pure unit` now.)
If you do not need access to the argument `#!hs ev`, `#!hs Select.always` provides a simple shortcut for `#!hs const <<< Just`:
```hs
HE.onMouseOver $ Select.always $ Select.highlight (Index index)
```
## Returning non-unit values
Use `#!hs map` or `#!hs <$` or `#!hs pure` to return other types of values from a query. So, instead of something like this:
```hs
H.subscribe $ eventSource' someEventSource
\value -> Just (SetVisibility value H.Listening)
```
Use
```hs
H.subscribe $ eventSource' someEventSource
\value -> Just $ setVisibility value $> H.Listening
```
or
```hs
H.subscribe $ eventSource' someEventSource
\value -> Just do
setVisibility value
pure H.Listening
```
!!! info ""
Many thanks to [Nicholas Scheel](https://github.com/MonoidMusician) for providing the implementation of `QueryF` and the documentation above.

23
docs/examples.md Normal file
View file

@ -0,0 +1,23 @@
# Examples
You can play around with a few example components here. However, for a much richer set of components with much more functionality, check out the [Ocelot design system by CitizenNet](https://citizennet.github.io/purescript-ocelot/#typeaheads).
### Dropdown
Dropdowns are a common button-driven input type, especially for navigation. But most custom dropdowns sacrifice usability: unlike browser default dropdowns, you can't type on most custom dropdowns, nor are many built with accessibility in mind. With `Select` you can easily create rich, usable dropdowns with little code.
<div data-component="dropdown" class="ocelot-scoped"></div>
Curious how to build a dropdown with `Select`? Check out [the dropdown tutorial](https://citizennet.github.io/tutorials/build-a-dropdown).
### Typeahead / Autocomplete
This library was originally designed so that we could build typeaheads with all sorts of custom rendering and functionality. It was frustrating to find solutions that almost worked, but broke down as soon as you needed a moderate level of customization.
Building typeaheads with `Select` is only a little more complex than building dropdowns. Instead of a button as input, you'll use a text input, and you'll be responsible for deciding how to handle user searches. `Select` handles debouncing user input, keyboard navigation, and more on your behalf.
The typeahead below is quite simple; to see examples of more sophisticated typeaheads -- including ones that fetch and display data asynchronously -- check out the [Ocelot component library](https://citizennet.github.io/purescript-ocelot/#typeaheads).
<div data-component="typeahead" class="ocelot-scoped"></div>
Curious how to build a typeahead with `Select`? Check out [the typeahead tutorial](https://citizennet.github.io/tutorials/build-a-typehead).

View file

@ -0,0 +1,3 @@
# How to Embed Parent Queries
One of the most powerful ways to extend `Select` is to write functionality in your component, triggered by a query, and then embed that query into the `Select` component.

45
docs/index.md Normal file
View file

@ -0,0 +1,45 @@
# Welcome
`Select` helps you build selection user interfaces in PureScript with Halogen. You can use it to build dropdowns, typeaheads and autocompletes, date pickers, image pickers, and more, with features like keyboard navigation, accessibility, and state management handled for you. This library takes a unique approach to component design to ensure you can leverage its features without compromising your design in any way.
## Installation
You can use `Select` in your PureScript project with a compatible package manager. The PureScript community typically leverages [psc-package](https://github.com/purescript/psc-package) or Bower:
```sh
# Using psc-package
$ psc-package install halogen-select
# Using Bower
$ bower install --save purescript-halogen-select
```
## Quick Start
If this is your first time using `Select`, start with the [tutorials](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started). I'd recommend starting with the simplest example where you'll learn to make this dropdown component:
<div class="ocelot-scoped" data-component="dropdown"></div>
!!! tip
Don't want to build your own UI components? Check out the [Ocelot component library](https://citizennet.github.io/purescript-ocelot)!
If this isn't your first time, you'll find value in these resources:
* The [how-to](https://citizennet.github.io/purescript-halogen-select/how-to/embed-parent-queries) section contains plenty of short guides for common tasks you'll perform using `Select`. Consider it a grab-bag of useful strategies and examples you can refer to when developing new components.
* The [concepts](https://citizennet.github.io/purescript-halogen-select/concepts/understanding-free-queries) section contains more detailed explanations on the design of the library. It will help you understand how to make good design choices and make the most of the tools available to you.
* The [reference documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select) contains the module documentation and source code. It's a useful reference to verify what functions are available to you.
* The [examples folder on Github](https://github.com/citizennet/purescript-halogen-select) contains the working source code for all the components in the [tutorials](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started). If you're building a similar component, this code can help you get started.
## Why Select?
`Select` provides essential behaviors for selection UI as a flexible, extensible Halogen component. But you won't find a single render function in the code. Instead, with a few helper functions, you can write your own `#!hs State -> HTML` function however you'd like. You can:
* Extend the component's functionality by embedding new queries in the HTML
* Extend the component's data by including as much additional state from the parent as you want (which you can then use in your render function)
* Leverage the provided features for user interaction, state management, accessibility, and logic
* Retain complete freedom over the design and aesthetic of your selection component
!!! aside "For visual learners"
I gave a talk at the Los Angeles PureScript meetup in April 2018 about the approach this library takes. It provides an overview of our design approach, including advantages and drawbacks, as well as a simple walkthrough of building a dropdown. No, the man in the preview isn't me -- that's [Phil Freeman](http://functorial.com/), the designer of the PureScript programming language.
<iframe width="560" height="315" src="https://www.youtube.com/embed/igWrktC0m7E?rel=0&amp;showinfo=0&amp;start=2119" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>

View file

@ -1,6 +0,0 @@
# Documentation
This section of the repository holds example components built with `Select` and demonstrated in a Github Pages site. These are only the source files; the build itself is managed by Circle CI and artifacts are kept on the `gh-pages` branch.
TODO:
- Write a post explaining how other PureScript repositories can do the same

View file

@ -1,12 +0,0 @@
# The base URL pages are relative to
base_url = "/purescript-halogen-select/"
# Don't provide or compile from Sass folder
compile_sass = true
# Turn on syntax highlighting
highlight_code = true
highlight_theme = "inspired-github"
# Put custom variables in [extra]
[extra]

View file

@ -1,17 +0,0 @@
+++
title = "PureScript Halogen Select"
description = "Some description"
template = "home.html"
+++
## Typeaheads
Use input-driven selects to support typeaheads and autocompletion.
{{ halogen(id="typeahead", header="Typeahead") }}
## Dropdowns
Use toggle-driven selects to support dropdowns.
{{ halogen(id="dropdown", header="Dropdown") }}

View file

@ -1,5 +0,0 @@
+++
title = "Documentation"
description = "Some description"
template = "section.html"
+++

View file

@ -1,5 +0,0 @@
+++
title = "Functions"
description = "Some description"
template = "article.html"
+++

View file

@ -1,5 +0,0 @@
+++
title = "Tutorials"
description = "Some description"
template = "section.html"
+++

View file

@ -1,46 +0,0 @@
+++
title = "Typeahead Tutorial"
description = "A tutorial for building typeaheads with Select."
template = "article.html"
+++
# Some Text
I've written an article here.
```hs
eval = case _ of
ToContainer q a -> H.query unit q *> pure a
HandleContainer m a -> case m of
C.Emit q -> eval q *> pure a
C.ItemSelected item -> do
H.modify \st -> st { selected = ( item : st.selected ) }
st <- H.get
_ <- H.query unit
$ H.action
$ C.ContainerReceiver
$ { render: renderContainer
, items: difference st.items st.selected }
pure a
Removed item a -> do
st <- H.get
let newSelections = delete item st.selected
newItems = difference newSelections st.items
H.modify (_ { selected = newSelections })
_ <- H.query unit
$ H.action
$ C.ContainerReceiver
$ { render: renderContainer
, items: newItems }
pure a
```
<br>
## Typeaheads
Support asynchronous, continuously asynchronous, and synchronous typeaheads with autocompletion.
{{ halogen(id="typeahead", header="Typeahead") }}

View file

@ -1,38 +0,0 @@
$black: #22292f;
$grey-95: #f0f1f2;
$grey-lightest: #f8fafc;
$step-1: 0.25rem;
$step-2: 0.5rem;
$step-4: 1.0rem;
$step-6: 1.5rem;
$semibold: 600;
$full: 100%;
$half: 50%;
pre {
padding: 0 $step-4 0 $step-4;
margin-top: $step-6;
margin-bottom: $step-6;
border-left: $grey-95 $step-1 solid;
}
table {
width: $full;
text-align: left;
border-collapse: collapse;
margin: $step-6 0;
}
th {
font-weight: $semibold;
padding: $step-2;
background-color: $grey-lightest;
}
td {
padding: $step-2;
border-top: $grey-95 1px solid;
}

View file

@ -1,6 +0,0 @@
{% extends "base-nav.html" %}
{% block page %}
{{ page.content | safe }}
{% endblock %}

View file

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% block content %}
<nav class="flex items-center justify-center py-6 mb-6">
<div class="flex items-center flex-no-shrink block mr-6">
<span class="font-light text-3xl"><a class="no-underline text-grey-darkest" href="{{ config.base_url }}">SELECT</a></span>
</div>
<div class="w-full block flex-grow flex items-center w-auto">
<div class="text-sm flex-grow">
<a class="block inline-block text-grey-dark no-underline hover:text-grey-darker mr-2" href="{{ config.base_url }}/tutorials">Tutorials</a>
<a class="block inline-block text-grey-dark no-underline hover:text-grey-darker mr-2" href="{{ config.base_url }}/documentation">Documentation</a>
</div>
<div>
<a class="bg-teal hover:bg-teal-lighter text-white border-2 border-teal hover:teal-lighter no-underline py-2 px-4 rounded mx-2"
href="https://github.com/citizennet/purescript-halogen-select">
GitHub
</a>
</div>
</div>
</nav>
{% block page %}
{% endblock %}
{% endblock %}

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en-us">
<head>
{% block head %}
<meta charset="UTF-8">
<title>{% block title %}{% if config.title %}{{ config.title }}{% else %}PureScript Halogen Select{% endif %}{% endblock title %}</title>
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}">
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.9.1/css/tachyons.min.css"/>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ config.base_url }}site.css" />
<script src="{{ config.base_url }}app.js"></script>
{% endblock head %}
</head>
<body class="font-normal text-grey-darkest leading-normal">
<div class="w-full max-w-lg mx-auto">
{% block content %}
{% endblock content %}
</div>
</body>
</html>

View file

@ -1,23 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ section.title }}{% endblock %}
{% block content %}
<div class="text-center">
<h1 class="font-light text-3xl pt-8">Select for PureScript's Halogen<br></h1>
<p class="text-xl text-grey-dark mb-4">Flexible, accessible primitives for selection user interfaces.</p>
<div class="my-8">
<a class="bg-teal hover:bg-teal-light text-white border-2 border-teal hover:border-teal-light no-underline py-2 px-4 rounded mx-2"
href="https://github.com/citizennet/purescript-halogen-select">
GitHub
</a>
<a class="bg-white hover:bg-teal-light hover:text-white text-teal border-2 border-teal-light no-underline py-2 px-4 rounded mx-2" href="{{ config.base_url }}/documentation">
Documentation
</a>
</div>
</div>
{{ section.content | safe }}
{% endblock %}

View file

@ -1,18 +0,0 @@
{% extends "base-nav.html" %}
{% block page %}
<h1>Table of Contents</h1>
<ul class="list-reset">
{% for page in section.pages %}
<li class="p-2">
<a class="text-blue no-underline" href="{{ page.permalink }}">
{{ page.title }}
</a>
</li>
{% endfor %}
</ul>
{{ section.content | safe }}
{% endblock %}

View file

@ -1,7 +0,0 @@
<div class="bg-grey-lighter flex flex-col items-center justify-center my-6 p-8">
{% if header %}
<h3 class="block font-light mb-6 text-grey-dark text-sm tracking-wide uppercase">{{ header }}</h3>
{% endif %}
<div class="flex flex-col items-center w-full" data-component-id="{{id}}"></div>
</div>

767
docs/tutorials/dropdown.md Normal file
View file

@ -0,0 +1,767 @@
# Let's Build a Dropdown
Dropdowns are among the simplest selection components you will build, but they can be tricky to get right. For example, you'll likely want to ensure that your users can type to highlight close text matches (like when you type "Ca" to highlight "California" in a state dropdown). You'll want to be accessible to folks using screen readers or keyboard-only navigation, too. And, of course, you'll want to achieve all this without compromising on your design.
In this tutorial, we'll build a dropdown together using `Select`. It will be ugly but functional; we won't worry much about CSS.
!!! note ""
This tutorial assumes you've followed the steps in the [Project Setup](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started/) section. While not necessary, this code is tested with those steps in mind.
It also assumes familiarity with the Halogen framework. If you need a refresher, try the official [Halogen guide](https://github.com/slamdata/purescript-halogen/tree/master/docs) or the [whirlwind tour of the component we're building from](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started/#a-whirlwind-tour-of-our-starter-component)
**If you are already an intermediate or advanced PureScript developer, then this tutorial will read slowly for you. Feel free to skim, get the gist of how the library works, and then move on to the much [more advanced typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead).**
Your code should work at the end of every step. If you run into issues or your code doesn't compile, please come visit us on the [PureScript user forum](https://purescript-users.ml) or the [#fpchat Slack channel](https://functionalprogramming.slack.com).
We're going to build a dropdown that is functionally equivalent this one:
<div class="ocelot-scoped" data-component="dropdown"></div>
Our component will look a little bit worse, because we're not going to spend time on CSS.
## Basic Setup
Let's get something on the screen!
The simplest sort of dropdown has a button that can toggle a menu open or closed, a list of items that can be selected from that menu, and zero, one, or more selected items. For our dropdown we'll assume that you can select at most one item, and that selecting an item will replace the text on the button with that item.
### Rendering a button and items
We'll start by rendering the button and the items. At this point our render function contains only an empty div, so let's fill in the rest of the HTML we need:
```hs
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
render st =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, HH.button_
[ HH.text "Click me to view some items" ]
, HH.ul_
[ HH.li_
[ HH.text "Item 1" ]
, HH.li_
[ HH.text "Item 2" ]
]
]
```
!!! warn ""
Make sure to compile this code and view the new output! You should see a header, a button, and two items in the list.
### A better `#!hs State` type
It's already clear we're going to need more than `#!hs Unit` for our `#!hs State` type. We at least need to know three things:
- If the menu is toggled on or off
- The currently-selected item (if there is one)
- The list of items available for selection
We can represent each of these with simple types in our state:
```hs
type State =
{ isOpen :: Boolean
, selectedItem :: Maybe String
, availableItems :: Array String
}
```
Now that our state contains these three fields, we need to update our `#!hs initialState` function to produce the right type of values:
```hs
initialState :: Input -> State
initialState = const
{ isOpen: false
, selectedItem: Nothing
, availableItems:
[ "Item One"
, "Item Two"
, "Item Three"
]
}
```
Finally, lets update our render function to leverage the information now contained in `#!hs State`. If there's a selected item, that will be the button's text; if not, we'll fall back to a default message. If the menu is open, we'll list out the available items for selection.
For code clarity, we'll also break out the dropdown into its own helper function.
```hs
import Data.Maybe (fromMaybe)
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
render st =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, dropdown st
]
dropdown :: State -> H.ParentHTML Query ChildQuery ChildSlot m
dropdown =
HH.div_
[ HH.button_
[ HH.text $ fromMaybe "Click me to view some items" st.selectedItem ]
, if st.isOpen
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> st.availableItems
else HH.text ""
]
```
!!! tip ""
Since the dropdown has no behavior yet, try changing the `#!hs initialState` to set `#!hs isOpen` to `#!hs true` to verify your items are in fact being rendered out to the page.
It ain't pretty, but at this point we've got all the rendering we need for a basic dropdown! The next step is to actually wire in some behavior.
## Integrating the component
Let's integrate the `Select` component! In just a few steps, we'll turn our simple render function into a fully-functioning dropdown with keyboard navigation, toggling, debounced type-to-search, and several other features.
### On building components with Select
The key idea behind the `Select` library is to **provide behaviors, not rendering**. The core component the library exposes doesn't have a render function at all! Of course, all Halogen components require a render function to work, and `Select` is no different. *You* are expected to provide that render function.
Why?
When you write the render function, not the library, you get to decide exactly what your component will look and feel like. You can also control what queries to trigger from HTML and when, effectively allowing you to control the behavior of the component *without configuration*. You can even extend it with new behavior and new state by using information from your parent component. The end result is a much smaller library component with a lot more flexibility and power for you.
We just wrote the rendering we need for an (admittedly ugly) dropdown. The render function we just wrote can actually serve almost as-is as the render function for `Select`! All we have to do is mount the `Select` component, make a few tweaks to our render code, and then pass in a little configuration information. Let's do that next.
### Importing the Select component
The first thing we'll do is bring in the `Select` library in the first place.
```hs
import Select as Select
import Select.Utils.Setters as Setters
```
!!! tip ""
You can always [view the module documentation for Select on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0) or the [source code on GitHub](https://github.com/citizennet/purescript-halogen-select). This is useful when integrating with third-party components so that you can check out the `#!hs Input`, `#!hs State`, `#!hs Query`, and `#!hs Message` types.
Next, we need to update our `#!hs ChildSlot` and `#!hs ChildQuery` types. We're only going to have one dropdown so we can leave the child slot as `#!hs Unit`; we do need to add the `Select` component's query type to our `#!hs ChildQuery` synonym, however.
This code, unfortunately, won't work:
```hs
type ChildQuery = Select.Query
Error found:
Type synonym Select.Query is partially applied.
Type synonyms must be applied to all of their type arguments.
in type synonym ChildQuery
```
The compiler has noticed that `#!hs ChildQuery`, a type synonym, now partially applied. That's because `#!hs Select.Query`, itself a type synonym, takes several arguments as described in the [module documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:Query). Let's walk through each one:
```hs
type ChildQuery o item eff = Select.Query o item eff
```
`o` is *your* query type. Remember how you can embed your own queries into `Select`, and in that way extend the component's functionality? This is how. So we can fill in the first argument:
```hs
type ChildQuery item eff = Select.Query Query item eff
```
`item` is the type of whatever items you want to be selectable. Commonly these are strings, but can also be custom data types. Later on, in the [typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead), we'll see how powerful custom data types can be for rendering purposes. For our simple dropdown we'll simply specialize this to `#!hs String`:
```hs
type ChildQuery eff = Select.Query Query String eff
```
`eff` is the type of whatever effect rows your component leverages. `Select` performs some effects, like manipulating the DOM or using threads to perform debouncing on your behalf asynchronously, and it must verify that its effects match your parent component effects. We'll leave this as an argument for now, but when we mount the component, we'll provide some concrete effects.
What happens if we try to save this?
```hs
Error found in module Component :
Could not match kind
Type
with kind
# Control.Monad.Eff.Effect
while checking the kind of State -> ParentHTML Query ChildQuery ChildSlot m0
```
Whoops! Our `#!hs render` and `#!hs eval` functions expect `#!hs ChildQuery` to have the kind `#!hs Type`, but instead we've provided a type synonym that's still awaiting an argument of kind `#!hs # Effect` (read: a row of effects). We need to supply that argument. Let's update those two functions:
```hs
render :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
dropdown :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
eval :: Query ~> H.ParentDSL State Query (ChildQuery eff) ChildSlot Message m
```
Now that `Select` has been imported and we've updated our `ChildQuery` and `ChildSlot` types to support it, we can worry about what to do when we receive a message from the component.
### Mounting the component
We're finally ready to mount the `Select` component. Mounting any component in Halogen requires supplying a slot value, the component itself, the component's input, and the component's handler. We can put together all of these except for the input, which we haven't prepared yet.
Let's stub out our render function in preparation:
```hs
import Halogen.HTML.Events as HE
render :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
render st =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, HH.slot unit Select.component ?input (HE.input <<< const Nothing)
]
```
Right away we get an error:
```hs
Error in module Component:
Could not match type
eff7
with type
( dom :: DOM
, avar :: AVAR
| eff7
)
when trying to match type QueryF Query String eff7
with type QueryF Query String
```
This happened because `Select` uses the `AVAR` and `DOM` effects, but we've asserted our component will work with ANY row. That's not true anymore! Our component will now work with any row that includes `DOM` and `AVAR`. It's easy enough to fix. We need to define our own effects row, extensible by `eff`:
```hs
import Control.Monad.Aff.AVar (AVAR)
import DOM (DOM)
type Effects eff =
( dom :: DOM
, avar :: AVAR
| eff
)
```
Now we can update our various types to use our new row, verifying the effects are the same throughout the component. As a rule of thumb, anywhere a **function** uses `eff`, wrap it in our new `Effects` type synonym, but don't apply the same rule to types or type synonyms. As an example, we'll update `component`, but we won't update the `ChildQuery` type synonym:
```hs
component :: ∀ eff m
. MonadAff (Effects eff) m
=> H.Component HH.HTML Query Input Message m
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
dropdown :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
eval :: Query ~> H.ParentDSL State Query (ChildQuery (Effects eff)) ChildSlot Message m
```
With that out of the way, we can turn to filling in our component's input type. We can either look at the module documentation for `Select.Input` or look at the type error that resulted from our typed hole, `?input`. Both will tell us that we need to provide a value of this type:
```hs
type Input o item eff =
{ inputType :: InputType
, items :: Array item
, initialSearch :: Maybe String
, debounceTime :: Maybe Milliseconds
, render :: State item eff -> ComponentHTML o item eff
}
```
Let's build this up, step by step. First, we see we have to provide an `#!hs InputType`. This is described in the module documentation:
```hs
-- | Text-driven inputs will operate like a normal search-driven selection component.
-- | Toggle-driven inputs will capture key streams and debounce in reverse (only notify
-- | about searches when time has expired).
data InputType
= TextInput
| Toggle
```
We don't have any text input for our dropdown -- its a button -- so we'll go with the `#!hs Toggle` constructor.
```hs
selectInput :: Select.Input Query String (Effects eff)
selectInput =
{ inputType: Select.Toggle
, ...
}
```
Next, we're expected to provide an array of items. Fortunately we already have those in our `#!hs State`. We can just send those items directly into the input.
```hs
selectInput =
{ ...
, items: st.availableItems
}
```
Next, we're expected to provide an initial search. This would be more useful if we had a text input, but for our dropdown, we'll start off with no initial search.
```hs
selectInput =
{ ...
, initialSearch: Nothing
}
```
What about a debounce time? For toggle-driven components, this is how long to aggregate key presses before the user's typing should affect the list of items. For search-driven components, this is how long to delay before raising a message with the new search. For our dropdown, we don't care:
```hs
selectInput =
{ ...
, debounceTime: Nothing
}
```
Finally, we're expected to provide a render function to the component. Ah ha! We've actually already written a render function for a dropdown -- it's just that the type is wrong.
### Adapting the render function for `Select`
Let's look at the types side-by-side:
```hs
Select.render :: Select.State item eff -> Select.ComponentHTML o item eff
dropdown :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
dropdown st =
HH.div_
[ HH.button_
[ HH.text $ fromMaybe "Click me to view some items" st.selectedItem ]
, if st.isOpen
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> st.availableItems
else HH.text ""
]
```
From this, we can see that we need to use the state type from `Select` to drive our render function, not the state from our parent component. Will our function still work? Let's look at [`Select`'s state type in the module documentation](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:State) to see what we have available:
```hs
type State item eff =
{ inputType :: InputType
, search :: String
, debounceTime :: Milliseconds
, debouncer :: Maybe (Debouncer eff)
, inputElement :: Maybe HTMLElement
, items :: Array item
, visibility :: Visibility
, highlightedIndex :: Maybe Int
, lastIndex :: Int
}
```
That's a lot of stuff! We have some of the data we need in `Select`'s state -- we have our list of items and whether the menu is open or closed. We even got new information, like which item is highlighted. But we're missing something crucial: which item is selected.
As a general rule, `Select` does not manage selections on your behalf. You are expected to decide what you want to happen when an item is selected and to store the selections yourself.
What can we do? We don't have all the information we need to write this function. Or do we?
In fact, *so long as we write the `Select` render function within the `where` clause of the parent component's render function*, we have access to the parent's state! Let's give it a shot.
```hs
render parentState =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, HH.slot unit Select.component ?input (HE.input <<< const Nothing)
]
where
dropdown
:: Select.State String (Effects eff)
-> Select.ComponentHTML Query String (Effects eff)
dropdown childState =
HH.div_
[ HH.button_
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
, if childState.visibility == Select.On
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> childState.items
else HH.text ""
]
```
It works! Even better, we no longer have to manage things like `#!hs openState` in the parent anymore. Finally, now that we have the render function we need, we can finally finish our component's input type:
```hs
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
render parentState =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, HH.slot unit Select.component selectInput (HE.input <<< const Nothing)
]
where
selectInput :: Select.Input Query String (Effects eff)
selectInput =
{ inputType: Select.Toggle
, items: parentState.availableItems
, initialSearch: Nothing
, debounceTime: Nothing
, render: dropdown
}
dropdown = ...
```
## Integrating Behavior
Everything up to this point has been standard Halogen except for writing the child component's render function. At this point, the `Select` component is running -- good work! However, it's not yet *doing* anything.
It's now time to turn your static HTML into a fully-functioning dropdown.
### Attaching behavior to Select
`Select` works by using a few helper functions that attach at critical points in your render function. The library assumes very little about what your rendering looks like, except that there _at least_ exists:
- One or more items that can be selected
- An element that contains those items
- A focusable element that can be used to toggle visibility and capture keystrokes
Accordingly, you'll need to use three helper functions, each exported by the `Select.Utils.Setters` module:
- `setItemProps`
- `setContainerProps`
- `setToggleProps` (for toggle-driven input)
- `setInputProps` (for text-driven input)
Each of these functions should be used on the property array for the relevant element in your HTML. Let's walk through each one using our built render function.
First, let's augment our individual items. `#!hs setItemProps` takes an index and some properties and outputs some new properties, which include all sorts of event handlers necessary for keyboard events and click events to work. In order to provide it with the index it needs, we'll use the `#!hs mapWithIndex` function from `#!hs Data.Array`.
```hs
import Data.Array (mapWithIndex)
dropdown childState =
HH.div_
[ HH.button_
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
, case childState.visibility of
Select.Off -> HH.text ""
Select.On -> HH.ul [] $
mapWithIndex
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
childState.items
]
```
Next, we'll move to the element that contains the items. The `#!hs setContainerProps` function takes and returns some properties, attaching all the behavior the library needs. We'll use this on the parent element, `ul`:
```hs
dropdown childState =
HH.div_
[ HH.button_
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
, case childState.visibility of
Select.Off -> HH.text ""
Select.On -> HH.ul (Setters.setContainerProps []) $
mapWithIndex
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
childState.items
]
```
Finally, we can make sure that our button toggles the menu on and off, captures keyboard events, can be tabbed to, and all sorts of other stuff with the `#!hs setToggleProps` function.
```hs
dropdown childState =
HH.div_
[ HH.button
(Setters.setToggleProps [])
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
, case childState.visibility of
Select.Off -> HH.text ""
Select.On -> HH.ul [] $
mapWithIndex
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
childState.items
]
```
Whew! Your rendering code now contains everything it needs to provide a keyboard-accessible dropdown. If you open this up in the browser and click around, you'll notice it's properly toggling and can be tabbed to.
Let's make one last improvement. When you use your arrow keys on the dropdown, the highlighted index is changing, but since we didn't provide any CSS we can't see it. Let's add some bare-bones styling so we can watch the highlights:
```hs
import Halogen.HTML.Properties as HP
Select.On -> HH.ul (Setters.setContainerProps []) $
mapWithIndex
(\ix item ->
HH.li
( Setters.setItemProps ix
$ case Just ix == childState.highlightedIndex of
true -> [ HP.attr (HH.AttrName "style") "color: red;" ]
_ -> [] )
[ HH.text item ]
)
childState.items
```
There we go! Try toggling the menu on and off, using your arrow, enter, and escape keys, and so on. It works!
...almost. Alas, we aren't doing anything when the user makes a selection. `Select` is attempting to notify us that a selection occurred, but we never provided a handler. Let's fix that now.
### Handling messages from Select
When you add a new child component you invariably need to add a handler for its `#!hs Message` type. What should the parent do when something important has occurred in the child? To handle messages, add a new constructor to your query algebra that takes the child's `#!hs Message` type as an argument:
```hs
data Query a
= NoOp a
| HandleSelect Select.Message a
```
Ah -- this won't compile!
```hs
Error found in module Component:
Could not match kind
(Type -> Type) -> Type -> Type
with kind
Type
in type constructor Query
```
This looks similar to the type error we got when we tried to just use `Select.Query` in a type synonym. We need to provide a `#!hs Type` to `#!hs HandleSelect`, but `#!hs Select.Message` is still awaiting 2 arguments, the first of which is *itself* awaiting an argument! Let's go look at the [module documentation for `Select.Message`](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:Message).
```hs
data Message o item
```
We've seen both of these arguments before in the component's query type, so we should fill them in with the same values. `o` is our parent component query type, and `item` is a `#!hs String`:
```hs
data Query a
= NoOp a
| HandleSelect (Select.Message Query String) a
```
As soon as you save and rebuild you'll see another compiler error!
```hs
Error found in module Component
A case expression could not be determined to cover all inputs.
The following additional cases are required to cover all inputs:
(HandleSelect _ _)
in value declaration component
```
This time it's because we've added a new query, but we never updated our `#!hs eval` function to describe what should happen when the query is triggered. What should we actually _do_ when a message comes up from the child component?
!!! tip ""
You'll often see type errors that end in "... value declaration component." when the error occurred in any of the functions in the `where` clause for the component. It can be annoying to track down where the error actually is in your code. One way to help track these down is to move your code out of the `where` block and into the module level temporarily so the compiler can identify which particular function is causing the issue.
There are four possible sub-cases that we need to handle, each described in the module documentation:
```hs
data Message o item
= Searched String
| Selected item
| VisibilityChanged Visibility
| Emit (o Unit)
```
Let's stub out each of these cases and then decide what to do with them:
```hs
eval :: Query ~> H.ParentDSL State Query (ChildQuery eff) ChildSlot Message m
eval = case _ of
NoOp next -> pure next
HandleSelect message next -> case message of
Select.Searched string ->
pure next
Select.Selected item ->
pure next
Select.VisibilityChanged vis ->
pure next
Select.Emit query ->
pure next
```
Let's take these case-by-case.
What should we do when the user has searched something on our dropdown? This is just a simple list of items, so we'll simply ignore their search. We can leave this as `#!hs pure next`.
What should we do when the user has selected an item? Ah! This is more interesting. We want to set their item as the currently-selected one, and then we want to remove it from the available items list. Once we've removed it from the available items, we'll update `Select` with its new items to display and we'll toggle it off.
We can use `#!hs difference` from `#!hs Data.Array` to filter out the selected item from the overall list of items. This is a common pattern in `Select`: the parent holds the immutable list of all possible items, and `Select` receives some subset of those items at each render. You might use the user's search to filter out items in a typeahead, for example, or only load 50 results at a time into a dropdown.
```hs
import Data.Array (difference)
Select.Selected item -> do
st <- H.get
_ <- H.query unit $ Select.setVisibility Select.Off
_ <- H.query unit $ Select.replaceItems $ difference st.availableItems [ item ]
H.modify _ { selectedItem = Just item }
```
What should we do when the dropdown's visibility has changed? This can often be useful to run validation, but for our dropdown, we don't care what its visibility is. We can leave this as `#!hs pure next`.
Finally, what should we do when the child component raises its `Emit` message? What does this even mean? `Emit` exists so you can embed your own queries into `Select` and extend its behavior. Since the message contains one of your own queries, all you have to do is evaluate it: you can call `eval` recursively to run your query.
You can think of `Emit` as notifying you that the query you embedded is ready to run.
```hs
Select.Emit query -> do
eval query
pure next
```
Nice and simple! While you may write all kinds of logic for the other messages raised by `Select`, you'll always write this same code for the `Emit` message.
## Conclusion
Congratulations! You have successfully built a keyboard-navigable dropdown using `Select`. You integrated the library, wrote your own render function, and then augmented it with helper functions from the library. Then, you handled the output messages and sent queries to update the component's state. You've done quite a lot of work!
### Next Steps
This tutorial was a slow, thorough introduction to the `Select` library. But we've only scratched the surface of what you can do with it. I'd recommend continuing on to the faster-paced and more advanced [typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead).
### Source Code
If you'd like to use this component as a starting point from which to build your own, feel free to copy/paste the source code below.
??? article "Full source code for the tutorial"
```hs
module Component where
import Prelude
import Control.Monad.Aff.AVar (AVAR)
import Control.Monad.Aff.Class (class MonadAff)
import DOM (DOM)
import Data.Array (difference, mapWithIndex)
import Data.Maybe (Maybe(..), fromMaybe)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties (attr) as HP
import Select as Select
import Select.Utils.Setters as Setters
data Query a
= NoOp a
| HandleSelect (Select.Message Query String) a
type State =
{ isOpen :: Boolean
, selectedItem :: Maybe String
, availableItems :: Array String
}
type Input = Unit
type Message = Void
type ChildSlot = Unit
type ChildQuery eff = Select.Query Query String eff
type Effects eff =
( dom :: DOM
, avar :: AVAR
| eff
)
component :: ∀ eff m
. MonadAff (Effects eff) m
=> H.Component HH.HTML Query Input Message m
component =
H.parentComponent
{ initialState
, render
, eval
, receiver: const Nothing
}
where
initialState :: Input -> State
initialState = const
{ isOpen: false
, selectedItem: Nothing
, availableItems:
[ "Item One"
, "Item Two"
, "Item Three"
]
}
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
render parentState =
HH.div_
[ HH.h1_
[ HH.text "Dropdown" ]
, HH.slot unit Select.component selectInput (HE.input HandleSelect)
]
where
selectInput :: Select.Input Query String (Effects eff)
selectInput =
{ inputType: Select.Toggle
, items: parentState.availableItems
, initialSearch: Nothing
, debounceTime: Nothing
, render: dropdown
}
dropdown
:: Select.State String (Effects eff)
-> Select.ComponentHTML Query String (Effects eff)
dropdown childState =
HH.div_
[ HH.button
(Setters.setToggleProps [])
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
, case childState.visibility of
Select.Off -> HH.text ""
Select.On -> HH.ul (Setters.setContainerProps []) $
mapWithIndex
(\ix item ->
HH.li
( Setters.setItemProps ix
$ case Just ix == childState.highlightedIndex of
true -> [ HP.attr (HH.AttrName "style") "color: red;" ]
_ -> [] )
[ HH.text item ]
)
childState.items
]
eval :: Query ~> H.ParentDSL State Query (ChildQuery (Effects eff)) ChildSlot Message m
eval = case _ of
NoOp next -> pure next
HandleSelect message next -> case message of
Select.Searched string ->
pure next
Select.Selected item -> do
st <- H.get
_ <- H.query unit $ Select.setVisibility Select.Off
_ <- H.query unit $ Select.replaceItems $ difference st.availableItems [ item ]
H.modify _ { selectedItem = Just item }
pure next
Select.VisibilityChanged vis ->
pure next
Select.Emit query -> do
eval query
pure next
```

View file

@ -0,0 +1,255 @@
# Introduction
Halogen is a powerful framework for building PureScript applications. Its used by several companies, including SlamData and my own company, CitizenNet (a Condé Nast company), among others. The `Select` library is written for the Halogen framework, so if you dont know how to use Halogen yet, you ought to start with the [Halogen guide](https://github.com/slamdata/purescript-halogen/tree/master/docs). That said, with only passing familiarity with Halogen, you should be able to follow along just fine!
## Setup
Instead of creating a new Halogen project from scratch, well start with a minimal starter template. This template includes the HTML, build scripts, and basic `Main.purs` file necessary to run your Halogen application. It also includes a component with the bare minimum definitions in place. This component does nothing at all, which is nice because we can easily use it to start building dropdowns, typeaheads, and other components.
!!! note ""
We prefer Yarn over NPM for package management and scripts, but either one will work. Anywhere you see `#!sh yarn <script>`, you can substitute `#!sh npm run <script>` instead. Feel free to look at the `package.json` file if you want to see what these scripts are doing.
### Installation
First, clone the Halogen template project from CitizenNet, install dependencies, and make sure things build properly. If they dont, please reach out on the [Purescript user forum](https://purescript-users.ml) so we can fix it!
Next, make sure to install `Select`:
```shell
bower i --save purescript-halogen-select
```
And that's it! You now have everything you need to complete the tutorials. This is the full set of steps you can follow to get all set up:
```shell
# Get the CitizenNet starter Halogen project
git clone git@github.com:citizennet/purescript-halogen-template.git
# Change into the directory and install packages
cd purescript-halogen-template && yarn
# Install a new package: purescript-halogen-select
bower i --save purescript-halogen-select
# Build the project
yarn build
# Open the application in the browser
open dist/index.html
```
After you complete each step in the tutorial, make sure to rebuild the project and refresh your browser to see your updated component.
### Helpful tip: Watching for file changes
Its convenient to keep a terminal running which watches for file changes, rebuilds the project, and bundles JavaScript on your behalf. Then, when you make a change to a file, all you have to do is wait a moment and refresh the page to see your updates.
When I write PureScript, I usually work with two terminals open. I use the first to write code, and the second to watch those changes and rebuild. I recommend using the same technique as you walk through these tutorials. These three steps are all you need:
1. Open a new terminal and run the `#!sh watch` script
2. Open your editor to a source file
3. Open a new tab in your browser pointed to `dist/index.html` so you can see the app
To test everything is working, try editing `src/Component.purs` to change the title of the page. The project should automatically rebuild on save. Then, when you refresh the browser, you should see your new text rendered.
```shell
# Watch for changes and rebuild (remember to refresh the page after builds)
yarn watch
```
## A whirlwind tour of our starter component
The project starts off with a minimal Halogen component. As a brief refresher, I'll step through each of the types and functions involved.
!!! info
If you are already quite familiar with Halogen, feel free to skip this section entirely.
### Query Algebra
How does a component know what to do?
In Halogen, we give names to each computation we'd like a component to run. Computations that can have side effects but don't return anything are colloquially called *actions*; those that can have side effects and also return something are called *requests*. The type that lists out the possible actions and requests for a component is called the component's *query algebra*. The Halogen guide has a relevant [section about query algebras](https://github.com/slamdata/purescript-halogen/blob/master/docs/2%20-%20Defining%20a%20component.md#query-algebra) if you'd like to know more.
What actions and requests can our starter component perform? By looking at the query algebra, we see just one constructor:
```hs
data Query a
= NoOp a
```
All we know so far is that this component can do one thing: evaluate a query called `NoOp`. We'll see what it does later on when we look at the `#!hs eval` function.
### State
Every component encapsulates some state, described by its `#!hs State` type. You will usually see Halogen components use records to hold state, like this:
```hs
type State = { on :: Boolean, name :: String }
```
State is the core of your component. Most of the queries you see in Halogen components modify state in some way, and the render function that produces HTML for the component has only the `#!hs State` type as its argument.
For our starter component, we don't need any state just yet, so we've simply assigned it the `#!hs Unit` type. When we start building selection components, however, we'll soon create a record to hold our state.
```hs
type State = Unit
```
### Input
A component's `#!hs Input` type can be thought of as a container for any information you'd like to pass to the component. It's most commonly used to provide a component with some initial `#!hs State` values via the `#!hs initialState :: Input -> State` function. However, it's more powerful than that!
Once a Halogen component has been mounted to the DOM, there is only one way to continue sending it new information: its `#!hs Input` type paired with its `#!hs receiver` function. Every time the parent component re-renders, it will send a new `#!hs Input` to the child component.
For more information on the `#!hs Input` type, see the [Parent and Child Components](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md#input-values) section of the Halogen guide.
Our starter component doesn't need any input, so we'll assign it the `#!hs Unit` type. However, once we build a dropdown or typeahead, we'll probably want to receive the list of items that can be selected as input.
```hs
type Input = Unit
```
### Message
How does a component tell its parent when something important has happened? In Halogen, this is accomplished with a `#!hs Message` type. Like the query algebra, this is just a type describing messages that can be raised, containing some information. To actually trigger sending a particular message, you can use the `#!hs raise` function provided by Halogen.
When we start building selection components, we'll use messages to notify parent components when items have been selected or removed. Our starter component doesn't need to raise any messages, however, so we've given it the `#!hs Void` type.
??? note "Why are we using `Void` when we have no messages?"
Why use `Void` instead of `Unit` for the `Message` type when it has no constructors? This is common practice in Halogen because of how messages are used by parent components. When your component raises a message, it gets handled by the parent using a function like this:
`#!hs Child.Message -> Maybe (ParentQuery Unit)`
If you want to ignore all messages from the child, you could write an implementation like this:
`#!hs Halogen.HTML.Events.input <<< const Nothing`
However, if the child's message type is `Void`, then you can use the `absurd` function from `Data.Void`:
`#!hs absurd :: Void -> a`
This saves you a bit of typing when you mount a child component in a slot and makes it absolutely unambiguous that there are no messages to handle. It also ensures that if you add a message to the child component later on you'll get a compiler error -- this is a good thing!
Compare mounting a child component that uses `Unit` to represent "no messages" vs. using `Void`:
```hs
-- It's unclear whether you're ignoring all messages or whether there are
-- simply no messages to handle.
HH.slot ComponentSlot component unit (Halogen.HTML.Events.input <<< const Nothing)
-- It's obvious there are no messages, and if that changes (the component adds a
-- message) you'll get a nice compile-time error.
HH.slot ComponentSlot component unit absurd
```
For more information on messages, see the [Parent and Child Components](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md) section in the Halogen guide.
```hs
type Message = Void
```
### ChildQuery and ChildSlot
Halogen components often have further child components. To maintain type safety when managing multiple child components, Halogen uses a pair of concepts: *child queries* and *child slots*.
- The **ChildQuery** type lists out each unique type of child component your component has. For each type of child component, you'll add its query type here.
- The **ChildSlot** type works like an address book for the various child components. If you only have one child component of any distinct `ChildQuery`, then you can just use `unit`. However, if you have multiple children with the same query type, you need some way to distinguish between them. It's common to use custom types or integers for this.
See the [Multiple Types of Child Component](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md#multiple-types-of-child-component) section of the Halogen guide for more details.
For now, our component has no children. Once we bring in the `Select` component we'll update these types.
```hs
type ChildQuery = Const Void
type ChildSlot = Unit
```
### Component
Ah! We can finally create our component. The actual component definition is simple: we call the `#!hs parentComponent` function from Halogen to assert we're creating a component that can have further child components and provide it with the four functions it needs to operate. More on those in a moment!
```hs
component :: ∀ eff m
. MonadAff eff m
=> H.Component HH.HTML Query Input Message m
component =
H.parentComponent
{ initialState
, render
, eval
, receiver: const Nothing
}
where
```
Next, lets look at those function definitions, defined in the `#! where` clause:
#### initialState
The `#!hs initialState` function describes how to go from the component's `#!hs Input` type to its `#!hs State` type. In this case, our `#!hs State` type is just `#!hs Unit`, so we'll throw away the input and return `#!hs unit`.
```hs
initialState :: Input -> State
initialState = const unit
-- Could also be written this way:
initialState = id
```
#### render
The `#!hs render` function describes how to go from the component's `#!hs State` type to some HTML, where that HTML can include any of the components listed in the `#!hs ChildQuery` type. You'll use plenty of code from these modules when writing render functions:
```hs
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
```
We're going to spend a lot of time writing render functions in the following tutorials. You can refer to the Halogen guide's [section on rendering](https://github.com/slamdata/purescript-halogen/tree/master/docs) for more information.
For now we won't render anything to the page, represented by an empty div.
```hs
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
render st = HH.div_ []
```
#### eval
The `#!hs eval` function describes what to do when one of the queries from the component's query algebra is called. There are various ways a query can be triggered:
- The parent component can trigger a child component query using the `#!hs query` function
- A user can trigger a query with an event in HTML, like `#!hs onClick`
- The `#!hs eval` function can recursively call itself while evaluating a query
The `#! eval` function is where you get to actually define what all your queries *do*. Unlike the render function, you can actually perform all kinds of side effects here, like make API calls, update state, trigger queries in child components, raise messages, and more.
As usual, our starter component won't do much in its `#!hs eval`. When it receives the `#!hs NoOp` constructor, it will do nothing and return the next query.
```hs
eval :: Query ~> H.ParentDSL State Query ChildQuery ChildSlot Message m
eval = case _ of
NoOp next -> pure next
```
#### receiver
The `#!hs receiver` function describes what to do when a parent component sends in new `#!hs Input`. Its type signature looks like this:
```hs
receiver :: Input -> Maybe (Query Unit)
```
Once a Halogen component has been mounted, the only way to send it new input is via its `#!hs receiver` function. When its parent re-renders, it will automatically send the child component's input type again, and it's up to the `#!hs receiver` function to decide what to do with it.
This function can either provide a query to call, or `#!hs Nothing` if you'd like to ignore new input. If you elect to provide a query then you unlock all the power available in the `eval` function and can describe all sorts of things to do on new input, like making API calls or updating state.
In our case, we don't care about new input, so we'll ignore the input and return `#!hs Nothing`.
```hs
{ ...
, receiver: const Nothing
}
```

View file

@ -0,0 +1,88 @@
module Docs.CSS where
import Prelude
import Halogen.HTML as HH
baseContainer :: Array HH.ClassName
baseContainer = HH.ClassName <$>
[ "bg-white"
, "border-grey-80"
, "border-l-2"
, "border-r-2"
, "w-full"
]
selectionContainer :: Array HH.ClassName
selectionContainer = baseContainer <>
( HH.ClassName <$>
[ "border-t-2"
]
)
itemContainer :: Array HH.ClassName
itemContainer = baseContainer <>
( HH.ClassName <$>
[ "absolute"
, "shadow"
, "max-h-80"
, "overflow-y-scroll"
, "pin-t"
, "pin-l"
, "z-50"
, "border-b-2"
]
)
menu :: Array HH.ClassName
menu = HH.ClassName <$> [ "relative z-50" ]
ul :: Array HH.ClassName
ul = HH.ClassName <$> [ "list-reset" ]
li :: Array HH.ClassName
li = HH.ClassName <$>
[ "px-3"
, "rounded-sm"
, "text-black-20"
, "group"
, "hover:bg-grey-97"
, "cursor-pointer"
]
button :: Array HH.ClassName
button = HH.ClassName <$>
[ "no-outline"
, "px-4"
, "py-2"
, "!active:border-b"
, "active:border-t"
, "disabled:opacity-50"
, "disabled:cursor-default"
, "bg-blue-88"
, "border-blue-88"
, "hover:!disabled:bg-blue-82"
, "focus:bg-blue-82"
, "text-white"
, "rounded"
]
input :: Array HH.ClassName
input = HH.ClassName <$>
[ "bg-white"
, "border-t-2"
, "border-b-2"
, "font-light"
, "cc-blue-88"
, "border-grey-80"
, "disabled:bg-grey-95"
, "disabled:text-grey-70"
, "focus:no-outline"
, "py-2"
, "transition-1/4-bounce"
, "border-l-2"
, "border-r-2"
, "w-full"
, "px-3"
, "focus:border-blue-88"
, "!focus:!disabled:hover:border-grey-70"
]

View file

@ -14,12 +14,22 @@ import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Select as Select
import Select.Utils.Setters as Setters
import Docs.CSS as CSS
type Effects eff = ( avar :: AVAR, dom :: DOM, console :: CONSOLE | eff )
type State = { items :: Array String, text :: String }
type Input = { items :: Array String }
data Query a = HandleSelect (Select.Message Query String) a
data Message = Void
type State =
{ items :: Array String
, text :: String }
type Input =
{ items :: Array String }
data Query a
= HandleSelect (Select.Message Query String) a
data Message
= Void
type ChildSlot = Unit
type ChildQuery eff = Select.Query Query String eff
@ -45,8 +55,7 @@ component =
HandleSelect (Select.Selected item) a -> do
st <- H.get
_ <- H.query unit $ Select.setVisibility Select.Off
_ <- H.query unit $ Select.triggerBlur
_ <- H.query unit $ Select.replaceItems (difference st.items [ item ])
_ <- H.query unit $ Select.replaceItems $ difference st.items [ item ]
H.modify _ { text = item }
pure a
@ -57,7 +66,7 @@ component =
-> H.ParentHTML Query (ChildQuery (Effects e)) ChildSlot m
render st =
HH.div
[ class_ "w-full" ]
[ HP.class_ $ HH.ClassName "w-full" ]
[ HH.slot unit Select.component input (HE.input HandleSelect) ]
where
input =
@ -68,38 +77,36 @@ component =
, render: renderDropdown
}
class_ :: ∀ p i. String -> H.IProp ( "class" :: String | i ) p
class_ = HP.class_ <<< HH.ClassName
renderDropdown :: Select.State String (Effects e) -> Select.ComponentHTML Query String (Effects e)
renderDropdown state = HH.div_ [ renderToggle, renderContainer ]
renderDropdown state = HH.div_ [ renderToggle, renderMenu ]
where
renderToggle =
HH.button
( Setters.setToggleProps props )
( Setters.setToggleProps [ HP.classes CSS.button ] )
[ HH.text st.text ]
where
props = [ class_ "bg-blue hover:bg-blue-dark text-white font-bold py-2 px-4 rounded-sm w-full flex" ]
renderContainer =
HH.div [ class_ "relative z-50" ]
renderMenu =
HH.div [ HP.classes CSS.menu ]
$ if state.visibility == Select.Off
then []
else [ renderItems $ renderItem `mapWithIndex` state.items ]
else [ renderContainer $ renderItem `mapWithIndex` state.items ]
where
renderItems html =
renderContainer html =
HH.div
( Setters.setContainerProps props )
[ HH.ul [ class_ "list-reset" ] html ]
where
props = [ class_ "absolute bg-white shadow rounded-sm pin-t pin-l w-full" ]
( Setters.setContainerProps [ HP.classes CSS.itemContainer ] )
[ HH.ul [ HP.classes CSS.ul ] html ]
renderItem index item =
HH.li
( Setters.setItemProps index props )
[ HH.text item ]
where
props = [ class_
$ "px-4 py-1 text-grey-darkest"
<> if state.highlightedIndex == Just index then " bg-grey-lighter" else ""
props =
[ HP.classes
( CSS.li <>
if state.highlightedIndex == Just index
then [ HH.ClassName "bg-grey-lighter" ]
else []
)
]

View file

@ -18,6 +18,8 @@ import Halogen.HTML.Properties as HP
import Select as Select
import Select.Utils.Setters as Setters
import Docs.CSS as CSS
type TypeaheadItem = String
type Effects eff = ( avar :: AVAR, dom :: DOM, console :: CONSOLE | eff )
@ -134,7 +136,7 @@ renderInputContainer :: ∀ e
renderInputContainer state = HH.div_ [ renderInput, renderContainer ]
where
renderInput = HH.input $ Setters.setInputProps
[ class_ "rounded-sm bg-white w-full flex py-2 px-3"
[ HP.classes CSS.input
, HP.placeholder "Type to search..." ]
renderContainer =

View file

@ -1,6 +1,6 @@
-- | A centralized module ready for use to mount components into documentation pages.
module Docs.App.Component where
module Docs.Internal.Component where
import Prelude

View file

@ -1,7 +1,7 @@
-- | A proxy that hides both the Query and Message of wrapped component.
-- | Adapted from `Halogen.Component.Proxy` and `Halogen.Storybook.Proxy`.
module Docs.App.Proxy
module Docs.Internal.Proxy
( ProxyS
, proxy
) where

View file

@ -28,22 +28,23 @@ import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.VDom.Driver (runUI)
import Docs.App.Proxy (ProxyS, proxy)
import Docs.App.Component as Component
import Docs.Internal.Proxy (ProxyS, proxy)
import Docs.Internal.Component as Component
-- Finds all nodes labeled "data-component-id" and retrieves the associated attribute.
-- Then, mounts the right component at each node.
main :: ∀ eff. Eff (HA.HalogenEffects (Component.Effects eff)) Unit
main = HA.runHalogenAff do
elements <- awaitSelectAll $ { query: QuerySelector "div[data-component-id]", attr: "data-component-id" }
elements <- awaitSelectAll
{ query: QuerySelector "div[data-component]"
, attr: "data-component"
}
flip traverse_ elements $ \e -> runUI app e.attr e.element
----------
-- Routes
type ComponentQuery = ProxyS (Const Void) Unit
type Components m = Map.Map String (H.Component HH.HTML ComponentQuery Unit Void m)
routes :: ∀ eff m. MonadAff ( Component.Effects eff ) m => Components m

87
mkdocs.yml Normal file
View file

@ -0,0 +1,87 @@
# Project information
site_name: 'Select Documentation'
site_description: 'Building blocks for common selection user interfaces in PureScript & Halogen'
site_author: 'Thomas Honeyman'
site_url: 'https://citizennet.github.io/purescript-halogen-select'
# Repository
repo_name: 'purescript-halogen-select'
repo_url: 'https://github.com/citizennet/purescript-halogen-select'
# Copyright
copyright: 'Copyright &copy; 2018 CitizenNet'
# Config
theme:
name: 'material'
custom_dir: 'docs/theme'
language: 'en'
palette:
primary: 'cyan'
accent: 'cyan'
font:
text: 'Open Sans'
code: 'Roboto Mono'
logo:
icon: 'school'
# To mount components
extra_javascript:
- js/app.js
# To style components
extra_css:
- css/cn-tailwind.scoped.css
# Google Analytics
# google_analytics:
# - 'UA-XXXXXXXX-X'
# - 'auto'
# Extensions
# Recommended here:
# https://squidfunk.github.io/mkdocs-material/getting-started/#extensions
markdown_extensions:
- admonition
- codehilite:
guess_lang: false
linenums: true
- toc:
permalink: true
- pymdownx.arithmatex
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.critic
- pymdownx.details
- pymdownx.emoji:
emoji_generator: !!python/name:pymdownx.emoji.to_svg
- pymdownx.inlinehilite
- pymdownx.magiclink
- pymdownx.mark
- pymdownx.smartsymbols
- pymdownx.superfences
- pymdownx.tasklist:
custom_checkbox: true
- pymdownx.tilde
# Site architecture
pages:
- Home: 'index.md'
# - Design Philosophy: 'index.md'
- Tutorials:
- 'Project setup': 'tutorials/getting-started.md'
- 'Build a dropdown': 'tutorials/dropdown.md'
# - 'Build a typeahead': 'index.md'
# - 'Build a date picker': 'index.md'
- How To:
- 'How to embed parent queries': 'how-to/embed-parent-queries.md'
# - 'How to leverage the parent state for rendering': 'index.md'
# - 'How to watch a key on the parent state': 'index.md'
# - 'How to understand common type errors': 'index.md'
# - 'How to write your own setProps functions': 'index.md'
- Concepts:
# - 'Advice for writing better render functions': 'index.md'
# - 'Controlling component behavior from HTML': 'index.md'
- 'Understanding free queries': 'concepts/understanding-free-queries.md'
- Examples: 'examples.md'

View file

@ -1,13 +1,12 @@
{
"private": true,
"scripts": {
"build-all": "yarn run build-docs && yarn run build-site",
"watch-all": "pulp -w --before 'yarn run build-docs' build -I docs/src --to docs/dist/app.js",
"clean": "rm -rf output bower_components node_modules",
"postinstall": "bower i --silent",
"build-docs": "pulp build -I docs/src --to docs/dist/app.js",
"watch-docs": "pulp -w build -I docs/src --to docs/dist/app.js",
"build-site": "cd docs/site && gutenberg build && cd ../ && cp -r site/public/. dist/"
"build-docs": "pulp build -I examples --to docs/js/app.js",
"watch-docs": "pulp -w --then 'mkdocs serve' build -I examples --to docs/js/app.js",
"clean": "rm -rf output bower_components node_modules site docs/js docs/css",
"postinstall": "bower i --silent && yarn fetch-css && yarn move-css",
"fetch-css": "curl https://cdn.rawgit.com/citizennet/purescript-ocelot/dev/dist/cn-tailwind.scoped.css --output cn-tailwind.scoped.css",
"move-css": "mkdir -p docs/css/ && mv cn-tailwind.scoped.css docs/css/"
},
"devDependencies": {
"pulp": "^12.0.1",

View file

@ -2,36 +2,28 @@
`Select` provides flexible building blocks for selection interfaces in Halogen. If you need a dropdown menu, typeahead, autocomplete, multi-select, calendar, image picker, or other selection interface, and you want it to be accessible, and you also want complete visual control over the component, then you're in the right place.
- [Module Documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select)
- [Official Documentation / Tutorials / Getting Started](https://citizennet.github.io/purescript-halogen-select)
- [CitizenNet UI Repository](https://github.com/citizennet/purescript-ocelot)
- [Module Documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select)
# Get Started / Learn More
There are a few ways to get started with the `Select` library.
**Installation**
**Installation**
`Select` is available on Bower and Pursuit:
```sh
# Using psc-package
psc-package install halogen-select
# Using bower
bower i --save purescript-halogen-select
```
**Examples**
You can see working examples of components built using this library in a few places:
For more information, try the [official documentation](https://citizennet.github.io/purescript-halogen-select).
- See the [Components](https://github.com/citizennet/purescript-halogen-select/tree/master/docs/src/Components) folder from the documentation site, or
- Check out the official [CitizenNet UI Repository](https://github.com/citizennet/purescript-ocelot) where we use this library to make our app's design system.
Have an example of a component you've built with `Select`? Open a PR or drop us a message and we can help review and showcase your work.
To build the documentation site locally:
1. Download the static site builder [Gutenberg](https://www.getgutenberg.io)
2. Build the static html with `npm run -s build-site`
3. Build the PureScript with `npm run -s build-docs`
4. Open the `index.html` file in `docs/dist`
# Design Principles
# Design Principles
The library provides essential behaviors for selection user interfaces as a group of Halogen components. But you won't find a single render function in the code. Instead, with the help of a few `setProps` helpers, you can write your HTML rendering however you'd like. You can freely include your own queries and the library will return them to be run. You can even use any data you want from your parent state in your render functions. The library manages user interaction, state, accessibility, and logic; you are responsible for rendering HTML depending on that state.
@ -67,73 +59,6 @@ For example, you can make your container compatible with the component with the
> Warning: If your events are duplicated by ours, they will be overwritten and fail to trigger.
# Free Monad Queries
Most of the time Halogen queries look like this:
```purescript
data QueryF (… other type arguments omitted …) a
= ...
| SetVisibility Visibility a
| GetVisibility (Visibility -> a)
```
(where `QueryF` is used directly as the Halogen query functor)
This library takes a slightly different approach: the query functor is actually `Control.Monad.Free.Free QueryF`, the [free monad](https://pursuit.purescript.org/packages/purescript-free/4.2.0/docs/Control.Monad.Free) generated by the query functor.
This allows queries the full power of monadic (and applicative) composition: sequencing effects, determining control flow based on previous results, and my favorite: doing nothing (`pure unit`).
We now define smart query constructors for this Free pattern like so:
```purescript
-- | Set the container visibility (`On` or `Off`).
setVisibility :: ∀ o item eff. Visibility -> Query o item eff Unit
setVisibility v = liftF (SetVisibility v unit)
-- | Get the container visibility (`On` or `Off`). Most useful when sequenced
-- | with other actions.
getVisibility :: ∀ o item eff. Query o item eff Visibility
getVisibility = liftF (GetVisibility id)
```
## Different patterns
In the simple cases, the helpers Halogen use to work with raw query constructors are folded into the smart Free query constructors already: `H.action (SetVisibility On)` becomes simply `setVisiblity On`, and similarly `H.request GetVisibility` is just `getVisibility`. This is because these patterns are typically present already smart constructors: `setVisibility` returns `Free QueryF Unit`, since it is an action, and `getVisibility` returns `Free QueryF Visibility`, since it requests the visibility. This allows for easy composition in `do` notation:
```purescript
toggleVisibility = do
vis <- getVisibility
setVisibility (not vis)
```
Cest très facile!
Event handlers look a little different. This is one example:
```purescript
HE.onMouseDown \ev -> Just do
Select.preventClick ev
Select.select index
when doBlur Select.triggerBlur
```
(Of course you may return `Nothing` if you so wish, but its effect is just like `pure unit` now.)
If you do not need access to the argument `ev`, `Select.always` provides a simple shortcut for `const <<< Just`:
```purescript
HE.onMouseOver $ Select.always $ Select.highlight (Index index)
```
## Returning non-unit values
Use `map` or `<$` or `pure` to return other types of values from a query. So, instead of something like this:
```purescript
H.subscribe $ eventSource' someEventSource
\value -> Just (SetVisibility value H.Listening)
```
Use
```purescript
H.subscribe $ eventSource' someEventSource
\value -> Just $ setVisibility value $> H.Listening
```
or
```purescript
H.subscribe $ eventSource' someEventSource
\value -> Just do
setVisibility value
pure H.Listening
```
## Inspiration & Thanks