Skip to content

Commit 2d1c435

Browse files
authored
Lingering mast additions (#151)
* Add session cleanup system. Add toggle for diff logs on the client. Improve return attribute handling. * Get head and body programmatically instead of assuming location * Add mast docs * Add to guides folder
1 parent 9247448 commit 2d1c435

File tree

4 files changed

+502
-106
lines changed

4 files changed

+502
-106
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# %mast, shrubbery edition:
2+
3+
### A framework for building reactive Web clients as shrubs, purely in hoon.
4+
5+
---
6+
7+
### Introduction
8+
9+
To understand the %mast front-end model, imagine the client as a shrub within your ship, instead of an application on the browser. A mast shrub is the source of truth for the state of your front-end, and it should contain all client-side logic to transition this state. In most cases, your mast shrub will act as an interface that renders the state of some other shrub.
10+
11+
Mast shrubs are spawned and managed by %mast (which is itself a shrub). %mast will handle everything related to connecting with and updating the browser. It does this by sending a script along with your UI that communicates with %mast, applying its diffs and handling any events. Your shrub needs to do only two things: have `manx` state, and take `ui-event` pokes. Your shrub will also likely have a `%src` dependency, which provides your back-end data (though it's not necessary).
12+
13+
Whenever you change the `manx` state of your shrub, %mast will automatically sync the browser with this state. All display updates happen like this; you never need to explicitly poke anything to %mast or interact with the browser.
14+
15+
%mast will poke your shrub with a `ui-event`, which represents an event that happens on the browser, such as a click or a form submit. A ui-event is of the type `[=path data=(map @t @t)]`. The path is an identifier for the event that you will have written in an attribute in your Sail. When handling a ui-event poke, you can switch over this path to implement your event handler logic. The data map contains any data that you might return from the browser with the event.
16+
17+
---
18+
19+
### In your Sail
20+
21+
%mast uses three main attributes: `event`, `return`, and `key`, along with `debounce`, `throttle`, `js-on-event`, `js-on-add`, and `js-on-delete`.
22+
23+
#### The event attribute
24+
25+
The `event` attribute lets you add event listeners to elements. The value of the `event` attribute is a path where the first segment is the name of an event, followed by any number of segments. %mast will add the specified event listener to the element, and when the event is triggered your shrub will receive a `ui-event` poke.
26+
27+
- Refer to this page for a list of events: https://developer.mozilla.org/en-US/docs/Web/API/Element#events
28+
29+
An element with an event attribute looks like this:
30+
31+
```
32+
;div(event "/click/example-event");
33+
```
34+
35+
- Note: you can add multiple event listeners on a single element by writing multiple paths separated by whitespace.
36+
37+
If you are adding an event to a dynamically generated element (e.g. producing a list of elements with ++turn) where there might be multiple elements created with a singularly defined event attribute, a useful technique is to add a dynamic segment to the path containing some information identifying the element. For example:
38+
39+
```
40+
;div(event "/click/example-event/{some-id}");
41+
```
42+
43+
When handling the click event in this example, you could pattern match the path like this: `[%click %example-event @ta ~]` and use the third segment to identify the element in your event handling logic.
44+
45+
#### The return attribute
46+
47+
The `return` attribute lets you to specify data to return from the event. This data will be contained in the data map of the `ui-event` poke. Using this attribute requires some knowledge of the DOM API (i.e. element and event object properties: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model). If you only need form data instead of general purpose data on an event, you can use the form element submit event (see the section on implementing forms below).
48+
49+
The value of the `return` attribute is also written as a path. A number of paths may be written, separated by whitespace. The first segment of the path refers to the object on the client to return data from. There are three options:
50+
51+
+ `"/target/..."` for the user’s current target, which is always the element on which this event was triggered. (e.g. the text input the user is focused on.),
52+
+ `"/event/..."` for the event object,
53+
+ `"/your-element-id/..."` for any other element by id.
54+
55+
The second segment of the path is the property to return from the object. For example: `"/target/value"` or `"/my-element/textContent"` or `"/event/clientX"`.
56+
57+
For instance, you can add a click event attribute to a div, and return the x coordinate of the mouse on click using using the return attribute like this:
58+
59+
```
60+
;div
61+
=event "/click/get-x-coordinate"
62+
=return "/event/clientX"
63+
;
64+
==
65+
```
66+
67+
- Note: the property name is exactly the same as what you would access on the object in JavaScript, so you will need to use camel case in some situations.
68+
69+
#### Implementing forms
70+
71+
There are two ways to implement forms in %mast. You can either use a %mast `return` attribute to return the values of inputs on event, or you can simply add a "/submit/..." `event` attribute on your form element, and the value of each input in the form will be included in the data map of the event poke with each input's `name` attribute as its key.
72+
73+
For example, in your Sail you can implement a form like this:
74+
75+
```
76+
;form(event "/submit/my-form")
77+
;input(name "my-input");
78+
;button: Submit
79+
==
80+
```
81+
82+
#### The key attribute
83+
84+
The `key` attribute is not necessary to use, but it is best practice when you have a list of elements that will change.
85+
86+
A `key` is a globally unique value which identifies the element (two distinct elements in your Sail should never have the same key). %mast adds location based keys to your elements by default, but when you provide information about the identity of the element by specifying the `key`, it allows %mast to make more efficient display updates.
87+
88+
Keys are most relevant in situations where you are dynamically generating elements, e.g.
89+
90+
```
91+
;div
92+
;* %+ turn my-list
93+
|= =some-data
94+
;div(key "{<id.some-data>}");
95+
==
96+
```
97+
98+
#### Other attributes
99+
100+
The attributes `debounce` and `throttle` let you add debouncing and throttling to events when placed on an element with an `event` attribute. These attributes take a number value for their duration in seconds.
101+
102+
For example, you can add debounce to an input that sends its value when the user types:
103+
104+
```
105+
;input
106+
=event "/input/get-value"
107+
=return "/target/value"
108+
=debounce "0.7"
109+
;
110+
==
111+
```
112+
113+
Using throttle, you can make use of rapid-fire events like mousemove:
114+
115+
```
116+
;div
117+
=event "/mousemove/handle-move"
118+
=return "/event/clientX /event/clientY"
119+
=throttle "0.5"
120+
;
121+
==
122+
```
123+
124+
The `js-on-event`, `js-on-add`, and `js-on-delete` events allow you to run arbitrary JavaScript when placed on an element that either has an `event` triggered on it, when the element is added to the DOM through a diff, or deleted through a diff.
125+
126+
For example, with js-on-event you can immediately add a class to an element on an event instead of waiting for an update from your ship:
127+
128+
```
129+
;div
130+
=id "my-id"
131+
=event "/click/js-example"
132+
=js-on-event "document.getElementById('my-id').classList.add('clicked')"
133+
;
134+
==
135+
```
136+
137+
---
138+
139+
### Shrub components
140+
141+
Any mast shrub that you write can also function as a component within some other mast shrub. This lets you split up your interface into composable and reusable building blocks for rendering your shrubs, which each serve as standalone UIs.
142+
143+
Whether your mast shrub exists on the client nested as a component, as a standalone UI, or both simultaneously, within your ship it exists as the same shrub and you do not need to treat it any differently in your code.
144+
145+
To add a shrub component in your Sail, write an element where the name is prefixed with `imp_` followed by the name of your shrub's /imp file. This element also needs to have a text node that encodes the `pith` of the %src dependency that your shrub renders. These component elements can be dynamically added or removed like any other element.
146+
147+
For example:
148+
149+
```
150+
;imp_my-mast-shrub: /pith/to/src
151+
```
152+
153+
---
154+
155+
### Examples
156+
157+
The %mast related IO in your shrub would look essentially like this:
158+
159+
```
160+
++ poke
161+
|= [=stud:neo =vase]
162+
^- (quip card:neo pail:neo)
163+
?+ stud !!
164+
::
165+
%ui-event
166+
=/ event !<(ui-event vase)
167+
?+ path.event !!
168+
::
169+
[%click %my-button ~]
170+
:: handle the click event ...
171+
::
172+
[%submit %my-form ~]
173+
:: get form data from the map:
174+
=/ data=@t (~(got by data.event) 'my-input-name')
175+
:: handle the form ...
176+
::
177+
==
178+
::
179+
%rely
180+
:: on a change in your dependency's state
181+
:: produce new manx, and update the shrub's manx state:
182+
`manx/!>((render-my-sail bowl))
183+
::
184+
==
185+
```
186+
187+
And the corresponding Sail would look something like this:
188+
189+
```
190+
++ render-my-sail
191+
|= =bowl:neo
192+
^- manx
193+
;html
194+
;head;
195+
;body
196+
;button(event "/click/my-button"): click me
197+
;form(event "/submit/my-form")
198+
;input(name "my-input-name");
199+
==
200+
==
201+
==
202+
```
203+
204+
Here is a simple front-end for the diary shrub:
205+
206+
```
207+
/@ ui-event
208+
/@ txt
209+
/@ diary-diff
210+
^- kook:neo
211+
=<
212+
|%
213+
++ state pro/%manx
214+
++ poke (sy %ui-event %rely ~)
215+
++ kids *kids:neo
216+
++ deps
217+
^- deps:neo
218+
%- my
219+
:~ :^ %src & [pro/%diary (sy %diary-diff ~)]
220+
:+ ~ %y (my [[|/%da |] only/%txt ~] ~)
221+
==
222+
++ form
223+
^- form:neo
224+
|_ [=bowl:neo =aeon:neo =pail:neo]
225+
::
226+
++ init
227+
|= pal=(unit pail:neo)
228+
^- (quip card:neo pail:neo)
229+
::
230+
:: initialize the shrub's manx state.
231+
::
232+
`manx/!>((render bowl))
233+
::
234+
++ poke
235+
|= [sud=stud:neo vaz=vase]
236+
^- (quip card:neo pail:neo)
237+
?+ sud !!
238+
::
239+
%ui-event
240+
=/ event !<(ui-event vaz)
241+
::
242+
:: for a ui-event poke, switch over its path,
243+
:: and implement your event handlers.
244+
::
245+
?+ path.event !!
246+
::
247+
:: handling a form submit event that adds a diary entry:
248+
:: get form data from the data.event map by input name,
249+
:: and produce a poke for the diary back-end.
250+
::
251+
[%submit %diary-form ~]
252+
=/ data=@t (~(got by data.event) 'diary-input')
253+
=/ diff=diary-diff [%put-entry now.bowl data]
254+
=/ dest=pith:neo p:(~(got by deps.bowl) %src)
255+
:_ pail
256+
:~ [dest %poke diary-diff/!>(diff)]
257+
==
258+
::
259+
:: handling a button click event that deletes a diary entry:
260+
:: the path contains the id of the entry to delete,
261+
:: which is used to produce a delete poke for the back-end.
262+
::
263+
[%click %delete @ta ~]
264+
=/ id=@da (slav %da i.t.t.path.event)
265+
=/ diff=diary-diff [%del-entry id]
266+
=/ dest=pith:neo p:(~(got by deps.bowl) %src)
267+
:_ pail
268+
:~ [dest %poke diary-diff/!>(diff)]
269+
==
270+
::
271+
==
272+
::
273+
:: this shrub rerenders any time its back-end data changes,
274+
:: e.g. when the above event handlers add or delete entries.
275+
:: simply update your shrub's manx state on %rely,
276+
:: and %mast will take care of syncing the client.
277+
::
278+
%rely
279+
`manx/!>((render bowl))
280+
::
281+
==
282+
--
283+
--
284+
::
285+
|%
286+
::
287+
++ render
288+
|= =bowl:neo
289+
=/ diary-data (get-data bowl)
290+
^- manx
291+
;html
292+
;head;
293+
;body
294+
::
295+
:: this event attribute adds a submit event
296+
:: and it corresponds to the first ui-event handler.
297+
::
298+
;form(event "/submit/diary-form")
299+
;textarea(name "diary-input");
300+
;button: Enter
301+
==
302+
;div
303+
;* %+ turn diary-data
304+
|= [id=@da =txt]
305+
^- manx
306+
;div(key <id>)
307+
;span: {(trip txt)}
308+
::
309+
:: this event attribute adds a click event
310+
:: and it corresponds to the second ui-event handler.
311+
::
312+
;button(event "/click/delete/{<id>}"):"x"
313+
==
314+
==
315+
==
316+
==
317+
::
318+
:: this helper function gets a list of diary entries
319+
:: from the %src dependency back-end.
320+
::
321+
++ get-data
322+
|= =bowl:neo
323+
^- (list [@da txt])
324+
=/ deps (~(got by deps.bowl) %src)
325+
%+ turn ~(tap by kid.q.deps)
326+
|= (pair iota:neo (axal:neo idea:neo))
327+
:- `@da`+.p
328+
!< txt q.pail:(need fil.q)
329+
::
330+
--
331+
```

0 commit comments

Comments
 (0)