mirror of
https://github.com/urbit/shrub.git
synced 2024-09-11 12:25:43 +03:00
Diary docs
This commit is contained in:
parent
a95f384f12
commit
5ce2adcffe
@ -1,318 +0,0 @@
|
||||
# Tutorial 1: Counter
|
||||
|
||||
One of the simplest shrubs imaginable is a counter that stores one number and takes one poke: increment the number.
|
||||
|
||||
By the end of this tutorial you'll understand the structure of a shrub, and how to write a trivial one of your own. This won't explain shrubbery from first principles — you don’t need to understand it from first principles — but you'll see how similar a shrub is to a Gall agent and where they differ.
|
||||
|
||||
You’ll also get a glimpse of how one shrub can accomodate various frontend interfaces. We’ll make a simple frontend for Sky, a prototype namespace browser.
|
||||
|
||||
## Counter in Gall and Shrubbery
|
||||
Here's the Gall agent we'll reimplement in shrubbery. It stores one number and takes one poke, `%inc`, to increment the number.
|
||||
|
||||
```hoon
|
||||
/+ dbug, default-agent, verb
|
||||
|%
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
==
|
||||
+$ state-0
|
||||
$: %0
|
||||
value=@ud
|
||||
==
|
||||
+$ counter-action
|
||||
$% [%inc ~]
|
||||
==
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
%+ verb &
|
||||
%- agent:dbug
|
||||
=| state-0
|
||||
=* state -
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-fail on-fail:def
|
||||
++ on-save
|
||||
!>(state)
|
||||
::
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
:- ~
|
||||
%= this
|
||||
state !<(state-0 old)
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?+ mark
|
||||
(on-poke:def mark vase)
|
||||
::
|
||||
%noun
|
||||
=/ act
|
||||
!<(counter-action vase)
|
||||
?+ -.act
|
||||
(on-poke:def mark vase)
|
||||
::
|
||||
%inc
|
||||
:- ~
|
||||
%= this
|
||||
value +(value)
|
||||
==
|
||||
==
|
||||
==
|
||||
--
|
||||
```
|
||||
|
||||
Here's the same thing in shrubbery.
|
||||
|
||||
```hoon
|
||||
/@ number
|
||||
/@ counter-diff
|
||||
^- kook:neo
|
||||
|%
|
||||
++ state pro/%number
|
||||
++ poke (sy %counter-diff ~)
|
||||
++ kids *kids:neo
|
||||
++ deps *deps:neo
|
||||
++ form
|
||||
^- form:neo
|
||||
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
|
||||
+* state !<(number state-vase)
|
||||
++ init
|
||||
|= old=(unit pail:neo)
|
||||
^- (quip card:neo pail:neo)
|
||||
`(need old)
|
||||
++ poke
|
||||
|= [=stud:neo vaz=vase]
|
||||
^- (quip card:neo pail:neo)
|
||||
?> =(%counter-diff stud)
|
||||
=/ act !<(counter-diff vaz)
|
||||
?> =(-.act %inc) :: XX can we remove this line?
|
||||
`number/!>(+(state))
|
||||
--
|
||||
--
|
||||
```
|
||||
|
||||
Let’s set up a fakeship that has `/base/app/neo.hoon`, the prototype “shrub runner”, then go over the code piece by piece.
|
||||
|
||||
## Counter shrub, explained
|
||||
Now let’s take a closer look at the counter shrub. You’ll find a version of `counter.hoon` with comments in your `/imp` folder.
|
||||
|
||||
```hoon
|
||||
:: /imp/counter.hoon
|
||||
/@ number
|
||||
/@ counter-diff
|
||||
```
|
||||
|
||||
These lines import two files from our `/pro` folder: `number.hoon` and `counter-diff.hoon`.
|
||||
|
||||
```hoon
|
||||
:: /pro/number.hoon
|
||||
,@ud
|
||||
```
|
||||
|
||||
```hoon
|
||||
:: /pro/counter-diff.hoon
|
||||
,[%inc ~]
|
||||
```
|
||||
|
||||
The folder structure you have to work with right now is messier than it will be in the final product. This is an artefact of prototyping shrubbery in a Gall agent in the `%base` desk.
|
||||
|
||||
The only folders you need to understand for this tutorial are `/pro`, `/imp`, and `/con`.
|
||||
- `/pro` for protocols. Like `/sur`, this is where your custom types live.
|
||||
- `/imp` for implementations. Like `/app`, this is where your Gall agent-like shrubs live.
|
||||
- `/con` for conversions. Like `/mar`, this is where you define rules for transforming nouns in your desk.
|
||||
|
||||
Let’s look at the rest of the `/imp` file.
|
||||
|
||||
```hoon
|
||||
/@ number
|
||||
/@ counter-diff
|
||||
::
|
||||
:: outer core of a shrub
|
||||
^- kook:neo
|
||||
|%
|
||||
::
|
||||
:: the state of counter is a %number, just a @ud
|
||||
++ state
|
||||
^- curb:neo
|
||||
[%pro %number]
|
||||
::
|
||||
:: the set of pokes that counter
|
||||
:: takes only contains %counter-diff
|
||||
++ poke
|
||||
^- (set stud:neo)
|
||||
(sy %counter-diff ~)
|
||||
::
|
||||
::
|
||||
:: counter does not "constrain" its children;
|
||||
:: any shrub can be made below this shrub in the
|
||||
:: namespace, they can have any state and any kids
|
||||
++ kids
|
||||
^- kids:neo
|
||||
*kids:neo
|
||||
::
|
||||
:: counter has no other shrubs as dependencies
|
||||
++ deps
|
||||
^- deps:neo
|
||||
*deps:neo
|
||||
::
|
||||
:: inner core of a shrub
|
||||
++ form
|
||||
^- form:neo
|
||||
:: treat this door's sample as boilerplate
|
||||
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
|
||||
::
|
||||
:: de-vase the state; we don't know what it is,
|
||||
:: in most cases it will be counter's old state
|
||||
+* state !<(number state-vase)
|
||||
::
|
||||
:: +init, like +on-init
|
||||
++ init
|
||||
::
|
||||
:: minimal +init, just returns the
|
||||
:: initial state passed in on %make
|
||||
|= old=(unit pail:neo)
|
||||
^- (quip card:neo pail:neo)
|
||||
[~ (need old)]
|
||||
::
|
||||
:: +poke, like +on-poke
|
||||
++ poke
|
||||
::
|
||||
:: a stud (e.g. %number or %counter-diff) is kind
|
||||
:: of like a mark, it only gets more complicated
|
||||
:: than that with types from other desks/ships
|
||||
|= [=stud:neo vaz=vase]
|
||||
::
|
||||
:: return a (list card:neo) and a
|
||||
:: pail, which is a (pair stud vase)
|
||||
^- (quip card:neo pail:neo)
|
||||
::
|
||||
:: assert that the poke's stud is %counter-diff,
|
||||
:: which protects counter from evil vases
|
||||
?> =(%counter-diff stud)
|
||||
=/ act
|
||||
!<(counter-diff vaz)
|
||||
?> =(-.act %inc)
|
||||
::
|
||||
:: return no cards and a pail
|
||||
[~ [%number !>(+(state))]]
|
||||
--
|
||||
--
|
||||
```
|
||||
|
||||
Once you’ve saved `/imp/counter.hoon`, run `|commit %base` and Neo will add it to its state. We can now interact with this shrub in the Dojo.
|
||||
|
||||
## Poking the shrub
|
||||
A `card:neo` is a `(pair pith note)`.
|
||||
|
||||
A `pith` is a list of head-tagged cells forming a typed path. This is the location of the shrub to which your card will be sent.
|
||||
* The path `/examples/counter/one` will be a pith `~[%examples %counter %one]`.
|
||||
* The path `/~sampel/examples/counter/one` will be a pith `~[[%p ~sampel] %examples %counter %one]`.
|
||||
* The path `/~sampel/examples/counter/1` will be a pith `~[[%p ~sampel] %examples %counter [%ud 1]]`.
|
||||
|
||||
A `note` is one of the four types of command any shrub will accept.
|
||||
|
||||
```hoon
|
||||
+$ note
|
||||
$% [%make made] :: create a shrub
|
||||
[%poke =pail] :: poke a shrub
|
||||
[%tomb cas=(unit case)] :: tombstone a shrub
|
||||
[%cull ~] :: ???
|
||||
==
|
||||
```
|
||||
|
||||
If the `pith` doesn’t correspond to the location of an existing shrub, you’ll have to make a shrub there before doing anything else.
|
||||
|
||||
Let’s `%make` a shrub at path `/examples/counters/one` from the Dojo, giving it an initial state of `0`. We’ll explain the structure of the `%make` note in more detail in the Diary tutorial.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %examples %counters %one] [%make %counter `[%number !>(0)] ~]]
|
||||
```
|
||||
|
||||
You should see `>> %make /examples/counters/one` in the Dojo if successful.
|
||||
|
||||
Now we can now send a `%poke` to the counter shrub at this path.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %examples %counters %one] [%poke [%counter-diff !>([%inc ~])]]]
|
||||
```
|
||||
|
||||
At time of writing there is no easy way to inspect the state of a shrub from the Dojo. We’ll just have to build a frontend and hope it all just works.
|
||||
|
||||
## Counter frontend in Sky
|
||||
Shrubbery aims to be interface-agnostic. One part of that vision is `/con` files, which make it possible to convert data from one backend type to any frontend type, and one frontend type to any backend type. Here are Counter’s `/con` files.
|
||||
|
||||
### Converting number to HTMX
|
||||
```hoon
|
||||
:: /con/number-htmx.hoon
|
||||
/@ number :: @ud
|
||||
/- feather-icons
|
||||
:- [%number %$ %htmx]
|
||||
|= =number
|
||||
|= =bowl:neo
|
||||
^- manx
|
||||
;div.p3.fc.g2.ac.br2
|
||||
;h1: Counter
|
||||
;p: {<number>}
|
||||
;form
|
||||
=hx-post "/neo/hawk{(en-tape:pith:neo here.bowl)}?stud=counter-diff"
|
||||
=hx-target "find .loading"
|
||||
=hx-swap "outerHTML"
|
||||
=head "inc"
|
||||
;button.bd1.br1.p2.b1.hover.loader
|
||||
;span.loaded: Increment
|
||||
;span.loading
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
==
|
||||
==
|
||||
```
|
||||
|
||||
This “converts” the `number` type to a `manx`, specifically targeting a frontend that uses the [HTMX](https://htmx.org/) library. You don’t need to know HTMX to build shrubbery frontends or to follow the rest of this tutorial. If you want to understand the HTMX above in more detail, see `/con/number-htmx.hoon` for line-by-line comments.
|
||||
|
||||
This isn’t a 1:1 conversion from one data type to another; we’re not converting Hoon `number=1` to JSON `{ "number": 1 }`. If a frontend asks for a `number` in the form of HTMX, we return some [Sail](https://docs.urbit.org/language/hoon/guides/sail) that interpolates the `number` in a basic interface consisting of a heading, the number, and one button to send an `%inc` poke to the Counter shrub.
|
||||
|
||||
### Converting Node to %counter-diff
|
||||
```hoon
|
||||
:: /con/node-counter-diff.hoon
|
||||
/@ node :: manx
|
||||
/@ counter-diff :: [%inc ~]
|
||||
/- manx-utils
|
||||
:- [%node %$ %counter-diff]
|
||||
|= =node
|
||||
^- counter-diff
|
||||
=/ mu ~(. manx-utils node)
|
||||
=/ head (?(%inc) (got:mu %head))
|
||||
[head ~]
|
||||
```
|
||||
|
||||
This is a more straightforward conversion from a dynamic XML node (in this case, HTMX), to a `%counter-diff`. Using the [manx-utils](https://github.com/tinnus-napbus/manx-utils) Hoon library for brevity, we extract the XML node’s `head` attribute (which has been converted to the term `%inc` on its way here) and use that to form the `%counter-diff`, which is `[%inc ~]`. See `/con/node-counter-diff.hoon` for line-by-line comments.
|
||||
|
||||
## Testing the Counter in Sky
|
||||
The Sky homepage shows you one tile for all of the shrubs who are the immediate children of your `/home` shurb, which was made for you upon booting `%neo` for the first time. You won’t see a Counter tile there because there is no `/counter` shrub beneath `/home`, so let’s make one.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %home %counter] [%make %counter `[%number !>(0)] ~]]
|
||||
```
|
||||
|
||||
If you refresh your browser you should now see a tile labelled “counter”. Click there to see the Counter frontend from the `/con` file and increment the state of the `/counter` shrub.
|
||||
|
||||
## Building on the Counter
|
||||
If you know your way around Gall, you should now be able to make some minor changes to the counter example above. Try the following:
|
||||
|
||||
* Initialize the shrub with a default state if the given `(unit vase)` in `+init` is empty.
|
||||
* Add more pokes like `%dec`, `%add`, and `%sub` on the backend.
|
||||
* Add those pokes to the frontend interface, with one button per poke.
|
@ -1,13 +1,14 @@
|
||||
# Tutorial 2: Diary
|
||||
# Chapter 2: Diary
|
||||
|
||||
Now that you understand the structure of a shrub, the natural next step is to look at a shrub with kids.
|
||||
|
||||
Unlike the Counter example, there is no equivalent way to implement `/imp/diary` in Gall. We’re now going to work directly with Urbit’s programmable, global, referentially transparent namespace — which is a lot of words to say [`+axal`](https://docs.urbit.org/language/hoon/reference/arvo#axal) — as a tool to read and write data.
|
||||
Unlike the Counter example, there is no equivalent way to implement `/imp/diary` in Gall. We’re now going to work directly with Urbit’s programmable, global, referentially transparent namespace as a tool to read and write data.
|
||||
|
||||
## /imp/diary
|
||||
|
||||
Diary is an app that lets you write timestamped diary entries that the frontend will show in a chronological feed. Here’s a succinct version of the app’s backend logic, which clocks in at 34 lines of code.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /imp/diary.hoon
|
||||
/@ txt :: @t
|
||||
/@ diary :: name=@t
|
||||
@ -47,13 +48,14 @@ Diary is an app that lets you write timestamped diary entries that the frontend
|
||||
Most of this should be legible after the first tutorial. The only new ideas are the `+kids` arm, the use of `bowl`, and `%tomb`.
|
||||
|
||||
## +kids
|
||||
|
||||
Every shrub is a node in one tree, with one “root shrub” at the top of that tree. Every shrub below the root shrub is either one of its immediate children or one of its descendants.
|
||||
|
||||
In its `+kids` arm, every shrub can define constraints for the shrubs below it in the namespace, whether that’s constraining the types of their state or the pokes they’ll accept from other shrubs.
|
||||
|
||||
Let’s expand on diary’s `+kids` arm.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /imp/diary.hoon
|
||||
::
|
||||
:: constrain shrubs below diary in the namespace
|
||||
@ -97,9 +99,10 @@ The `pish:neo` statically types the paths which we’ll allow to be created bene
|
||||
(It’s worth flagging that `$curb:neo` contains several combinatorial rules about state types. For example `[%pro %txt]` would mean “the state of the child shrub can be any type which is readily convertible into a `%txt` with a conversion we have available in the `/con` folder”. This gives us a clue as to how we could handle state transitions and interoperability over the lifetime of our shrub, but it’s outside the remit of this tutorial.)
|
||||
|
||||
## bowl:neo
|
||||
|
||||
Notice that the `src` in `bowl:neo` differs from `bowl:gall`. Here’s the new type in full.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /sur/neo.hoon
|
||||
+$ bowl
|
||||
$: src=[=ship =pith] :: a request's source ship and shrub
|
||||
@ -113,11 +116,12 @@ Notice that the `src` in `bowl:neo` differs from `bowl:gall`. Here’s the new t
|
||||
```
|
||||
|
||||
## Generating cards, tombstoning shrubs
|
||||
|
||||
We covered `card:neo` in the Counter tutorial, but this is the first time we’re seeing one generated within a shrub. Diary takes two pokes: `%put-entry`, to create a new diary entry, and `%del-entry` to tombstone one.
|
||||
|
||||
Here’s the `+poke` arm of the Diary shrub, expanded with comments.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /imp/diary.hoon
|
||||
++ poke
|
||||
|= [=stud:neo vax=vase]
|
||||
@ -194,9 +198,10 @@ The location of this shrub, `here.bowl`, is just a list. So it’s easy to `+wel
|
||||
The `%make` card has two mysteries: it initializes diary entries with an empty `%txt` implementation and an empty map of dependencies. We’ll punt on dependencies until the next tutorial. If you look at `/imp/txt.hoon`, it’s just a `~`. This is an *elegant short-term workaround* that allows us to shove `%txt`s into the namespace; if the `/imp/<foo>` file is just a `~`, `/app/neo` will notice this and substitute in a shrub that takes the state of the type defined in `/pro/<foo>`. Like everything else in these tutorials this is subject to change!
|
||||
|
||||
## Diary frontend
|
||||
|
||||
Like Counter, the Diary shrub just has two `/con` files to convert to and from an HTMX frontend within the Sky browser.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /con/diary-htmx.hoon
|
||||
/@ diary :: name=@t
|
||||
/- feather-icons
|
||||
@ -280,7 +285,7 @@ There’s only a bit more going on here than in Counter’s `/con/number-htmx.ho
|
||||
|
||||
The Diary frontend is a text box at the top with a list of entries generated from a list of the Diary shrub’s child `pith`s. Nowhere is a list of children in the state being passed from the backend to the frontend, like you might see with a Gall agent passing converting a `(list item)` to a JSON array through a mark in the event that a frontend has scried or subscribed on a path, all of which would require painstaking specification by the developer.
|
||||
|
||||
```hoon
|
||||
```
|
||||
:: /con/node-diary-diff.hoon
|
||||
/@ node :: manx
|
||||
/@ diary-diff :: ?([%put-entry id=@da txt=@t] [%del-entry id=@da])
|
||||
@ -298,5 +303,6 @@ The Diary frontend is a text box at the top with a list of entries generated fro
|
||||
Just like the `/con/node-counter-diff.hoon` in the Counter tutorial, all this does is extract attributes from an XML node, having been converted into a `manx`, and manually constructs the poke expected by the shrub.
|
||||
|
||||
## Building on Diary
|
||||
* Amend the `+kids` arm so that other apps could create shrubs like `/path/to/diary/<@da>/comments`.
|
||||
* Diary takes two pokes: `%put-entry` and `%del-entry`, but only one of these is supported on the frontend. Implement a `%del-entry` poke from the frontend.
|
||||
|
||||
- Amend the `+kids` arm so that other apps could create shrubs like `/path/to/diary/<@da>/comments`.
|
||||
- Diary takes two pokes: `%put-entry` and `%del-entry`, but only one of these is supported on the frontend. Implement a `%del-entry` poke from the frontend.
|
@ -1,613 +0,0 @@
|
||||
# Tutorial 4: Tasks
|
||||
Let’s take a look at the Tasks shrub. From within the Sky frontend, you can create, edit, reorder, and delete nested tasks and subtasks. Checking off all subtasks will mark the parent as complete, which involves some interaction between parent and child shrubs.
|
||||
|
||||
In this lesson we’ll see how shrubs keep their parents informed of state changes using the `%gift` poke. This is the most complex UI we’ve looked at yet, so we’ll also focus on the `/con` files.
|
||||
|
||||
## /imp/task.hoon
|
||||
Tasks only needs one `/imp` file: `task.hoon`. The Tasks frontend shows you some tasks that may or may not have other tasks as children.
|
||||
|
||||
```hoon
|
||||
/@ task :: [text=cord done=? order=(list path)]
|
||||
::
|
||||
:: $task-diff
|
||||
:: $% [%new =task prepend=?]
|
||||
:: [%edit text=cord done=?]
|
||||
:: [%oust =pith]
|
||||
:: [%reorder order=(list pith)]
|
||||
:: ==
|
||||
/@ task-diff
|
||||
=>
|
||||
::
|
||||
:: helper core
|
||||
|%
|
||||
::
|
||||
:: check if all kids are completed
|
||||
++ check-kids
|
||||
|= =bowl:neo
|
||||
^- ?
|
||||
?: =([~ ~] kids.bowl)
|
||||
%.y
|
||||
=/ piths ~(tap in ~(key by ~(tar of:neo kids.bowl)))
|
||||
%+ levy piths
|
||||
|= =pith
|
||||
=/ =task
|
||||
!< task
|
||||
q.pail:(need (~(get by ~(tar of:neo kids.bowl)) pith))
|
||||
done.task
|
||||
::
|
||||
:: assign a unique numerical ID to a new subtask
|
||||
++ assign-name
|
||||
|= =bowl:neo
|
||||
^- @ud
|
||||
?: =([~ ~] kids.bowl) 1
|
||||
=/ sorted-names=(list @ud)
|
||||
%- sort :_ lth
|
||||
%+ turn ~(tap by ~(tar of:neo kids.bowl))
|
||||
|= [=pith =idea:neo]
|
||||
+:(,[%ud @ud] (rear pith))
|
||||
=/ last-name=@ud (rear sorted-names)
|
||||
=/ name-missing=(list @ud)
|
||||
%+ skim (gulf 1 last-name)
|
||||
|= n=@ud
|
||||
=(~ (find ~[n] sorted-names))
|
||||
?~ name-missing +(last-name)
|
||||
(rear name-missing)
|
||||
--
|
||||
::
|
||||
:: outer core
|
||||
^- kook:neo
|
||||
|%
|
||||
::
|
||||
:: state is a %task
|
||||
++ state pro/%task
|
||||
::
|
||||
:: we accept %task-diff and %gift pokes
|
||||
++ poke (sy %task-diff %gift ~)
|
||||
::
|
||||
:: we define one generation of
|
||||
:: kids at /path/to/this/task/<@ud>
|
||||
++ kids
|
||||
:+ ~ %y
|
||||
%- ~(gas by *lads:neo)
|
||||
:~ :- [|/%ud |]
|
||||
[pro/%task (sy %task-diff %gift ~)]
|
||||
==
|
||||
++ deps *deps:neo
|
||||
++ form
|
||||
::
|
||||
:: inner core
|
||||
^- form:neo
|
||||
:: XX refactor sample to same as other tutorials
|
||||
|_ [=bowl:neo =aeon:neo stud:neo state-vase=vase]
|
||||
:: XX refactor this to default +init from counter
|
||||
++ init
|
||||
|= pal=(unit pail:neo)
|
||||
^- (quip card:neo pail:neo)
|
||||
`(need pal)
|
||||
++ poke
|
||||
|= [=stud:neo vax=vase]
|
||||
^- (quip card:neo pail:neo)
|
||||
=/ this !<(task state-vase)
|
||||
?+ stud !!
|
||||
%gift
|
||||
?: (check-kids bowl)
|
||||
[~ task/!>(this(done %.y))]
|
||||
[~ task/!>(this(done %.n))]
|
||||
::
|
||||
%task-diff
|
||||
=/ diff !<(task-diff vax)
|
||||
?- -.diff
|
||||
%new
|
||||
=/ name=@ud (assign-name bowl)
|
||||
=. order.this
|
||||
?: prepend.diff
|
||||
[~[ud/name] order.this]
|
||||
(snoc order.this `pith`[ud/name ~])
|
||||
=. done.this |
|
||||
:_ task/!>(this)
|
||||
:~ :- (welp here.bowl ~[ud/name])
|
||||
[%make %task `task/!>(task.diff) ~]
|
||||
==
|
||||
::
|
||||
%edit
|
||||
:- ~
|
||||
:- %task
|
||||
!>
|
||||
%= this
|
||||
text text.diff
|
||||
done ?: (check-kids bowl)
|
||||
done.diff
|
||||
%.n
|
||||
==
|
||||
::
|
||||
%oust
|
||||
=/ i (find [pith.diff ~] order.this)
|
||||
?~ i `task/!>(this)
|
||||
:_ task/!>(this(order (oust [(need i) 1] order.this)))
|
||||
:~ [(welp here.bowl pith.diff) [%tomb ~]]
|
||||
==
|
||||
::
|
||||
%reorder
|
||||
`task/!>(this(order order.diff))
|
||||
==
|
||||
==
|
||||
--
|
||||
--
|
||||
```
|
||||
|
||||
## The %gift poke
|
||||
In the `+poke` arm we declare this shrub takes a `%gift` as well as a `%task-diff`, but we don’t have to import `%gift`. This is a special poke like `%rely` that `/app/neo` gives us when another shrub’s state changes.
|
||||
|
||||
Our shrub receives a `%gift` poke every time the state of one of its descendants changes: only kids in the `%y` case, all descendants in the `%z` case.
|
||||
|
||||
```hoon
|
||||
++ state [%pro %task]
|
||||
++ poke (sy %task-diff %gift ~)
|
||||
```
|
||||
|
||||
Then we can handle the poke like any other. In this case, when `/imp/task` receives word that one of its kids’ state has changed, it checks to see if all of its subtasks (kids) are completed, then updates its own state accordingly.
|
||||
|
||||
```hoon
|
||||
?+ stud !!
|
||||
%gift
|
||||
:: check if all kid tasks are done
|
||||
=/ dun (check-kids bowl)
|
||||
[~ [%task !>(this(done dun, kids-done dun))]]
|
||||
::
|
||||
```
|
||||
|
||||
## Frontend
|
||||
Let’s look at the Tasks frontend in detail.
|
||||
|
||||
### Converting tasks to HTMX
|
||||
```hoon
|
||||
/@ task :: [text=cord done=? kids-done=? order=(list pith)]
|
||||
:: import /lib/feather-icons
|
||||
/- feather-icons
|
||||
:: declare that this is a conversion from task to HTMX
|
||||
:- [%task %$ %htmx]
|
||||
:: outer gate takes a task, inner gate takes a bowl:neo,
|
||||
:: so we can access here.bowl and kids.bowl in the ui
|
||||
|= t=task
|
||||
|= =bowl:neo
|
||||
::
|
||||
:: in this case, all sail rendering
|
||||
:: happens in helper arms
|
||||
|^
|
||||
shell
|
||||
::
|
||||
++ pith-tape
|
||||
:: pith to tape conversion
|
||||
|= =pith
|
||||
^- tape
|
||||
(en-tape:pith:neo pith)
|
||||
::
|
||||
++ shell
|
||||
::
|
||||
:: this "shell" div is a wrapper for the whole interface
|
||||
:: <div class="shell fc js af p2 wf p1 g5 ma" here="{<(pith-tape here.bowl)>}">
|
||||
;div.shell.fc.js.af.p2.wf.p1.g5.ma
|
||||
=here (pith-tape here.bowl)
|
||||
::
|
||||
:: return a <style> element with
|
||||
:: the styling in the +css arm
|
||||
;style: {style}
|
||||
::
|
||||
:: embed JavaScript code from +script for
|
||||
:: a more responsive frontend experience
|
||||
;+ script
|
||||
::
|
||||
:: render from the +task-title arm, which
|
||||
:: returns a header with the task's title
|
||||
;+ task-title
|
||||
:: <div class="fc g1 kids">
|
||||
;div.fc.g1.kids
|
||||
::
|
||||
:: render a <form> element with an ordered list of
|
||||
:: the child shrubs, which are all the top-level tasks
|
||||
;+ form-ordered-kids
|
||||
:: render the <form> that sends a
|
||||
:: poke to create a new task
|
||||
;+ form-create
|
||||
== :: </div>
|
||||
== :: </div>
|
||||
::
|
||||
++ style
|
||||
^~
|
||||
%- trip
|
||||
'''
|
||||
.shell {
|
||||
max-width: 650px;
|
||||
padding-bottom: 50vh;
|
||||
padding-top: 30px;
|
||||
}
|
||||
input[type="text"]:hover {
|
||||
cursor: text;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
input:checked {
|
||||
outline: none;
|
||||
accent-color: black;
|
||||
}
|
||||
'''
|
||||
::
|
||||
++ script
|
||||
:: <script>
|
||||
;script
|
||||
;+ ;/ %- trip
|
||||
'''
|
||||
// applies scrollIntoView() methods to the provided
|
||||
// to display it as specified
|
||||
function center(el) {
|
||||
el.scrollIntoView({
|
||||
block: "center",
|
||||
inline: "start",
|
||||
behavior: "instant"
|
||||
})
|
||||
};
|
||||
|
||||
// tell the user why a clicked checkbox
|
||||
// can't be marked as checked
|
||||
document.getElementById("alert").addEventListener("click", function(e){
|
||||
if (document.getElementById("alert").hasAttribute("readonly")){
|
||||
e.preventDefault();
|
||||
alert("Subtasks are not completed");
|
||||
}
|
||||
});
|
||||
'''
|
||||
== :: </script>
|
||||
::
|
||||
++ task-title
|
||||
^- manx
|
||||
:: <div class="fc g2 br1">
|
||||
;div.fc.g2.br1
|
||||
:: <h2 class="bold.s2.tc">
|
||||
;h2.bold.s2.tc
|
||||
; {(trip (@t text.t))}
|
||||
== :: </h2>
|
||||
== :: </div>
|
||||
::
|
||||
++ kids-check
|
||||
::
|
||||
:: check if all subtasks are completed
|
||||
|= =pith
|
||||
~& > pith
|
||||
^- ?
|
||||
=/ t
|
||||
!< task
|
||||
q.pail:(~(got of:neo kids.bowl) pith)
|
||||
~& >> t
|
||||
kids-done.t
|
||||
::
|
||||
::
|
||||
++ form-ordered-kids
|
||||
::
|
||||
:: <form> that keeps track of tasks order, sends %reorder
|
||||
:: poke if tasks are manually reordered by the user
|
||||
;form.fc.g1
|
||||
=hx-post "/neo/hawk{(pith-tape here.bowl)}?stud=task-diff"
|
||||
=head "reorder"
|
||||
=hx-indicator ".reorder-indicator"
|
||||
=hx-swap "none"
|
||||
;*
|
||||
::
|
||||
:: iterates over the list of piths in order.task
|
||||
%+ turn
|
||||
order.t
|
||||
|= =pith
|
||||
:: extract kid information at pith from kids.bowl
|
||||
:: and runs +part-kid on pith and kid data
|
||||
=/ kid (~(get of:neo kids.bowl) pith)
|
||||
?~ kid
|
||||
;div: does not exist {(pith-tape pith)}
|
||||
(part-kid [pith (need kid)])
|
||||
==
|
||||
::
|
||||
++ part-kid
|
||||
::
|
||||
:: sends %edit poke on input change
|
||||
|= [=pith =idea:neo]
|
||||
:: extracts information from idea:neo to task
|
||||
=/ =pail:neo pail.idea
|
||||
=/ t=task !<(task q.pail)
|
||||
:: converts pith to tape
|
||||
=/ pt (pith-tape (welp here.bowl pith))
|
||||
:: checks if task is done; if so, assigns
|
||||
:: attribute "done" to the manx created below
|
||||
=- ?. done.t -
|
||||
-(a.g [[%done ""] a.g.-])
|
||||
^- manx
|
||||
:: <div class="fc g1" here="{<pt>}" done="" ...>
|
||||
:: toggle hidden attribute on buttons-menu div
|
||||
;div.fr.g1.p1
|
||||
=here pt
|
||||
=onmouseover "this.childNodes[1].classList.remove('hidden');$(this).addClass('b1 br2');"
|
||||
=onmouseout "this.childNodes[1].classList.add('hidden');$(this).removeClass('b1 br2');"
|
||||
::
|
||||
:: div with %edit poke functionality
|
||||
:: div that sends %edit poke when input with class="text"
|
||||
:: or input with class "done" are being changed
|
||||
;div.fr.ac.g1.grow
|
||||
=hx-post "/neo/hawk{pt}?stud=task-diff"
|
||||
=hx-trigger "input changed delay:0.4s from:find .text, input from:find .done"
|
||||
=hx-swap "none"
|
||||
=head "edit"
|
||||
;+
|
||||
:: defines class attribute with class names
|
||||
=/ class [%class "p2 br1 border done s3"]
|
||||
::
|
||||
:: checkbox logic:
|
||||
:: - if task is toggled, checkbox will
|
||||
:: appear as checked
|
||||
:: - if task has kids and all kids are done,
|
||||
:: user will be free to toggle the task
|
||||
:: - if task have kids and they are not done,
|
||||
:: checkbox will have readonly attribute and
|
||||
:: will show alert onclick
|
||||
:: - even though we have logic for handling
|
||||
:: toggling of a parent task, we prevent
|
||||
:: a task from being marked as done if it
|
||||
:: has untoggled kids
|
||||
::
|
||||
:: combining attribute logic with manx below
|
||||
=; m
|
||||
:: checks if the task toggled as done
|
||||
?. done.t
|
||||
:: if it's not done, does it have kids?
|
||||
?~ order.t
|
||||
::
|
||||
:: a.g.m is a part of the manx that
|
||||
:: contains input checkbox attributes;
|
||||
:: we assign the class attribute to
|
||||
:: the rest of its data
|
||||
m(a.g [class a.g.m])
|
||||
=/ kc
|
||||
(kids-check pith)
|
||||
~& >>> kc
|
||||
?: kc
|
||||
:: assigning class attribute to
|
||||
:: the rest of manx data
|
||||
m(a.g [class a.g.m])
|
||||
::
|
||||
:: assigns readonly, id and class
|
||||
:: attributes to checkbox; id will trigger
|
||||
:: alert script functionality
|
||||
m(a.g [[%readonly ""] [%id "alert"] class a.g.m])
|
||||
::
|
||||
:: assigning checked and class attributes
|
||||
:: to the rest of manx data
|
||||
m(a.g [[%checked ""] class a.g.m])
|
||||
^- manx
|
||||
::
|
||||
:: onclick logic depending if input
|
||||
:: has been checked or unchecked:
|
||||
:: - if checked and doesn't have attribute
|
||||
:: readonly, adds attribute checked="" to
|
||||
:: local input and assigns classes "strike f3"
|
||||
:: to sibling input text
|
||||
:: - if it's been undone removes attribute checked=""
|
||||
:: and removes "strike f3" classes from sibling input
|
||||
::
|
||||
;input
|
||||
=type "checkbox"
|
||||
=name "done"
|
||||
=onclick (trip 'if (this.checked && !this.hasAttribute("readonly")){ this.setAttribute("checked", "");$(this.nextElementSibling).addClass("strike f3")} else {this.removeAttribute("checked");$(this.nextElementSibling).removeClass("strike f3")}')
|
||||
;
|
||||
==
|
||||
::
|
||||
:: combining class logic with
|
||||
:: manx below and make it in to an XML node
|
||||
;+ =; that
|
||||
=/ classes
|
||||
%+ weld
|
||||
"grow p2 br2 text bold"
|
||||
?:(done.t " strike f3" "")
|
||||
:: assigning classes attribute to the manx below
|
||||
that(a.g [[%class classes] a.g.that])
|
||||
^- manx
|
||||
::
|
||||
:: input has a task text value
|
||||
:: on input change, it will change the value
|
||||
:: attribute to update the div's POST request form
|
||||
;input
|
||||
=type "text"
|
||||
=name "text"
|
||||
=value (trip text.t)
|
||||
=onclick "$(this).addClass('border br2');$(this).removeClass('bold')"
|
||||
=onblur "$(this).addClass('bold');$(this).removeClass('border')"
|
||||
=oninput "this.setAttribute('value', this.value);"
|
||||
=onmouseover "$(this).addClass('b2 br2');"
|
||||
=onmouseout "$(this).removeClass('b2');"
|
||||
;
|
||||
==
|
||||
==
|
||||
:: buttons menu arm is called with pith as input
|
||||
;+ (buttons-menu pith)
|
||||
:: a-tag, opens subtask view with spinner logic on loading
|
||||
;a.p2.br1.hover.action.mono.fr.g2.loader
|
||||
=hx-indicator "this"
|
||||
=href "/neo/hawk{(pith-tape here.bowl)}{(pith-tape pith)}"
|
||||
=hx-swap "innerHTML"
|
||||
;span.b1.br1.p2.hfc.loaded: →
|
||||
;span.b1.br1.p2.hfc.loading
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ buttons-menu
|
||||
:: dropdown-menu arm provides reorder
|
||||
:: and oust(delete) logic for kids
|
||||
|= =pith
|
||||
^- manx
|
||||
::
|
||||
;div.p2.br1.fr.g2.hidden
|
||||
=hx-disinherit "hx-indicator"
|
||||
=style "padding-right:0px;"
|
||||
::
|
||||
:: indicator that %reorder POST request is in progress
|
||||
;div.reorder-indicator.p2.loader
|
||||
;span.loading.p1
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
:: moves kid div one task down and centers view
|
||||
:: and uses script logic to center user view on moved task
|
||||
;button.b1.br1.p2.hover.hfc
|
||||
=onclick "this.parentNode.parentNode.nextElementSibling?.insertAdjacentElement('afterend', this.parentNode.parentNode); center(this);"
|
||||
; ↓
|
||||
==
|
||||
:: moves kid div one task up and centers view
|
||||
;button.b1.br1.p2.hover.hfc
|
||||
=onclick "this.parentNode.parentNode.previousElementSibling?.insertAdjacentElement('beforebegin', this.parentNode.parentNode); center(this);"
|
||||
; ↑
|
||||
==
|
||||
:: delete button that sends POST request with %oust poke to parent task
|
||||
:: and after request been sent removes subtask div from DOM
|
||||
;button.b1.br1.p2.hover.loader.hfc
|
||||
=type "button"
|
||||
=hx-post "/neo/hawk{(pith-tape here.bowl)}?stud=task-diff"
|
||||
=hx-target "find .loading"
|
||||
=hx-swap "outerHTML"
|
||||
=hx-on--after-request "this.parentNode.parentNode.remove();"
|
||||
=head "oust"
|
||||
=pith (pith-tape pith)
|
||||
;span.loaded: delete
|
||||
;span.loading
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ form-create
|
||||
::
|
||||
:: form-create arm send POST request with data for %new poke
|
||||
:: depending on whether it's a parent task or a kid,
|
||||
:: it specifies a placeholder accordingly
|
||||
=/ placeholder ?:(?=([%ud @ud] (rear here.bowl)) "subtask" "task")
|
||||
^- manx
|
||||
;div.fc.g1.p4r
|
||||
=style "padding-top:8px;"
|
||||
:: form for %new poke POST request
|
||||
;form.fr.g1
|
||||
=hx-post "/neo/hawk{(pith-tape here.bowl)}?stud=task-diff"
|
||||
=head "new"
|
||||
=hx-swap "outerHTML"
|
||||
=hx-target "find button .loading"
|
||||
:: by default will append new task to the ordered kid list
|
||||
;input.hidden
|
||||
=name "prepend"
|
||||
=value "no"
|
||||
;
|
||||
==
|
||||
:: input for task text data
|
||||
;input.wf.p2.border.br2.grow
|
||||
=name "text"
|
||||
=autocomplete "off"
|
||||
=type "text"
|
||||
=required ""
|
||||
=placeholder placeholder
|
||||
=oninput "this.setAttribute('value', this.value);"
|
||||
;
|
||||
==
|
||||
:: button that triggers form to send request
|
||||
;button.b1.br1.hover.p1.wfc.loader
|
||||
;span.loaded: create
|
||||
;span.loading
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
==
|
||||
==
|
||||
--
|
||||
```
|
||||
|
||||
### Converting %task-diffs to HTMX
|
||||
```hoon
|
||||
/@ task-diff
|
||||
/- feather-icons
|
||||
:- [%task-diff %$ %htmx]
|
||||
|= t=task-diff
|
||||
|= =bowl:neo
|
||||
^- manx
|
||||
;div.loading
|
||||
=hx-get "/neo/hawk{(en-tape:pith:neo here.bowl)}"
|
||||
=hx-target "closest .hawk"
|
||||
=hx-indicator "closest .loader"
|
||||
=hx-swap "innerHTML"
|
||||
=hx-trigger "load"
|
||||
;span.loading
|
||||
;+ loading.feather-icons
|
||||
==
|
||||
==
|
||||
```
|
||||
|
||||
### Converting HTMX to %task-diffs
|
||||
```hoon
|
||||
/@ node :: manx
|
||||
::
|
||||
:: $task-diff
|
||||
:: $% [%new =task prepend=?]
|
||||
:: [%edit text=cord done=?]
|
||||
:: [%oust =pith]
|
||||
:: [%reorder order=(list pith)]
|
||||
:: ==
|
||||
/@ task-diff
|
||||
:: import lib/manx-utils
|
||||
/- manx-utils
|
||||
::
|
||||
:: declare that this is a conversion from a
|
||||
:: dynamic XML node to task-diff
|
||||
:- [%node %$ %task-diff]
|
||||
|= nod=node
|
||||
^- task-diff
|
||||
=/ mu ~(. manx-utils nod)
|
||||
:: extract head attribute from XML node
|
||||
=/ head (@tas (got:mu %head))
|
||||
%- task-diff
|
||||
?+ head
|
||||
~| [%unknown-head head]
|
||||
!!
|
||||
%new
|
||||
:: extract text and prepend attributes from XML node
|
||||
=/ text (vol:mu "text")
|
||||
=/ prepend (vol:mu "prepend")
|
||||
?: =(prepend 'prepend')
|
||||
:: construct the task-diff
|
||||
[head [text | ~] &]
|
||||
[head [text | ~] |]
|
||||
::
|
||||
%edit
|
||||
:: extract text attribute from XML node
|
||||
=/ text (vol:mu "text")
|
||||
:: extract checked attribute from done element in XML node
|
||||
=/ done-el (need (named:mu "done"))
|
||||
=/ done (~(has by (malt a.g.done-el)) %checked)
|
||||
:: construct the task-diff
|
||||
[head text done]
|
||||
::
|
||||
%oust
|
||||
:: extract pith attribute from XML node
|
||||
=/ path (stab (got:mu %pith))
|
||||
:: construct the task-diff
|
||||
[head (pave:neo path)]
|
||||
::
|
||||
%reorder
|
||||
=/ piths
|
||||
::
|
||||
:: extracting here attribute from each node in XML
|
||||
:: list and return as last element of here path
|
||||
%+ turn c.nod
|
||||
|= =manx
|
||||
=/ mu-reorder ~(. manx-utils manx)
|
||||
=/ here (get:mu-reorder %here)
|
||||
?~ here
|
||||
~& >>> [%bad-here manx]
|
||||
!!
|
||||
(pave:neo /[(rear (stab (crip (need here))))])
|
||||
:: construct the task-diff
|
||||
[head piths]
|
||||
==
|
||||
```
|
Loading…
Reference in New Issue
Block a user