on 657b415 help sign in
272 lines 8.3 kB history raw
1---
2next: true
3sort: 6
4title: Web Apps
5---
7We've done purely functional web pages as hook files, and we've
8done command line apps, so now let's interact with an app over
9the web. The fundamental concepts are no different than from the
10command line -- you send "poke" messages to apps and subscribe to
11data streams. The only difference is that you do it with
12javascript.
14Let's create a small web app that counts the number of times you
15poke it. There'll be a button to poke the app, and line of text
16below it saying how many times the app has been poked. We can
17update this by subscribing to that information from the app.
19Let's first write the app, `/ape/click.hoon`:
21```
22/? 314
23!:
24|%
25++ move ,[bone %diff %mark *]
26--
27!:
28|_ [hid=bowl clicks=@]
29++ poke-click
30 |= click=%click
31 ~& [%poked +(clicks)]
32 :_ +>.$(clicks +(clicks))
33 %+ turn (prey /the-path hid)
34 |=([o=bone *] [o %diff %clicks +(clicks)])
35++ peer-the-path
36 |= pax=path
37 [[[ost.hid %diff %clicks clicks] ~] +>.$]
38--
39```
41There's nothing really new here, except that we use a couple of
42new marks, `click` and `clicks`. When we get poked with a
43`click`, we increment the variable `clicks` in our state and tell
44all our subcribers the new value. When someone subscribes to us
45on the path `/the-path`, we immediately give them the current
46number of clicks.
48Let's take a look at the new marks. Here's `/mar/click.hoon`:
50```
51|_ click=%click
52++ grab
53 |%
54 ++ noun |=(* %click)
55 ++ json
56 |= jon=^json
57 ?> =('click' (need (so:jo jon)))
58 %click
59 --
60--
61```
63The mark `click` has hoon type `%click`, which means the only
64valid value is `%click`. We can convert from `noun` by just
65producing `%click`.
67We also convert from json by parsing a json string with `so:jo`,
68asserting the parsing succeeded with `need`, and asserting the
69result was 'click' with `?>`, which asserts that its first child
70is true.
72> Note the argument to `++json`is `jon=^json`. Why `^json`?
73> `++json` shadows the type definition, so if we want to refer to
74> the type, we have to prepend a `^`. This extends to multiple
75> levels: `^^^foo` means "not the innermost `foo`, nor the next
76> innermost, nor the next innermost, but the next innermost".
78> `++jo` in `zuse` is a useful library for parsing complex json
79> into hoon structures. In this case, the `:` between `so` and
80> `jo` means 'inside of', because `so` is an arm contained within
81> the core `jo`. Our case is actually simple enough that the
82> `?>` line could have been `?> =([%s 'click'] jon)`.
84We can test this mark from the command line:
86```
87~fintud-macrep:dojo> &click %click
88%click
89~fintud-macrep:dojo> &click &json [%s 'click']
90%click
91~fintud-macrep:dojo> &click &json &mime [/text/json (taco '"click"')]
92%click
93~fintud-macrep:dojo> &click &json &mime [/text/json (taco '"clickety"')]
94/~fintud-macrep/home/0/mar/click:<[7 5].[8 11]>
95ford: casting %json to %click
96ford: cast %click
97```
99And `/mar/clicks.hoon`:
101```
102|_ clicks=@
103++ grow
104 |%
105 ++ json
106 (joba %clicks (jone clicks))
107 --
108--
109```
111`clicks` is just an atom. We convert to json by creating an
112object with a single field "clicks" with our value.
114> Be sure to checkout section 3bD, JSON and XML, in zuse.hoon
115> `++joba` is just a function that takes a key-value pair and
116> produces a JSON object with one element.
118```
119~fintud-macrep:dojo> &json &clicks 6
120[%o p={[p='clicks' q=[%n p=~.^]]}]
121~fintud-macrep:dojo> &mime &json &clicks 6
122[[%text %json ~] p=12 q='{"clicks":6}']
123```
125Now that we know the marks involved, take another look at the app
126above. Everything should be pretty straightforward. Let's poke
127the app from the command line.
129```
130~fintud-macrep:dojo> |start %click
131>=
132~fintud-macrep:dojo> :click &click %click
133[%poked 1]
134>=
135~fintud-macrep:dojo> :click &click %click
136[%poked 2]
137>=
138```
140**Exercise**:
142- Modify `:sink` from the subcriptions chapter to listen to
143 `:click` and print out the subscription updates on the command
144 line.
146That's all that's needed for the back end. The front end is just
147some "sail" html and javascript. Here's `/pub/click/hymn.hook`:
149```
150;html
151 ;head
152 ;script(type "text/javascript", src "//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js");
153 ;script(type "text/javascript", src "/~~/~/at/home/lib/urb.js");
154 ;title: Clickety!
155 ==
156 ;body
157 ;div#cont
158 ;input#go(type "button", value "Poke!");
159 ;div#err(class "disabled");
160 ;div#clicks;
161 ==
162 ;script(type "text/javascript", src "click/main.js");
163 ==
164==
165```
167You should recognize the sail syntax from an earlier chapter.
168Aside from jquery, we also include `urb.js`, which is a framework
169for interacting with urbit.
171We have a button labeled "Poke!" and a div with id `clicks` where
172we'll put the number of clicks. We also include a small
173javascript file where the client-side application logic can be
174found. It's in `/pub/click/main.js`:
176```
177$(function() {
178 var clicks, $go, $clicks, $err
180 $go = $('#go')
181 $clicks = $('#clicks')
182 $err = $('#err')
184 window.urb.appl = "click"
186 $go.on("click",
187 function() {
188 window.urb.send(
189 "click", {mark: "click"}
190 ,function(err,res) {
191 if(err)
192 return $err.text("There was an error. Sorry!")
193 if(res.data !== undefined &&
194 res.data.ok !== undefined &&
195 res.data.ok !== true)
196 $err.text(res.data.res)
197 else
198 console.log("poke succeeded");
199 })
200 })
202 window.urb.bind('/the-path',
203 function(err,dat) {
204 clicks = dat.data.clicks
205 $clicks.text(clicks)
206 }
207 )
208})
209```
211We first set the default application to "click". A page can, of
212course, talk to multiple apps by specifying on each message which
213app to talk to.
215We set up two event handlers. When we click the button, we run
216`window.urb.send`, which sends a poke to an app. The arguments
217are data, parameters, and callback. The data is the specific
218data to send. The parameters are all optional, but here's a list
219of the available ones:
221- `ship`: target urbit. Defaults to `window.urb.ship`, which
222 defaults to the urbit which served the page.
223- `appl`: target app. Defaults to `window.urb.appl`, which
224 defaults to null.
225- `mark`: mark of the data. Defaults to `window.urb.send.mark`,
226 which defaults to "json".
227- `wire`: wire of the poke. Defaults to `/`.
229In our case, we specify only that the mark is "click".
231The callback funciton is called when we receive an
232acknowledgment. If there was an error, we put it in the `err`
233div that we defined above. Otherwise, we printf to the console a
234message saying the poke succeeded. A common pattern is to gray
235out a button when you click it and only reenable it when you get
236the positive acknowledgment.
238We also call `window.urb.bind`, which subscribes to a data stream
239on an app. It also takes three arguments: path, parameters, and
240callback. The path is the path on the app to subscribe to. The
241parameters are all optional (inded, we omit them entirely here),
242but are similar to those for `send`:
244- `ship`: target urbit. Defaults to `window.urb.ship`, which
245 defaults to the urbit which served the page.
246- `appl`: target app. Defaults to `window.urb.appl`, which
247 defaults to null.
248- `mark`: mark of expected data. Data of other marks is
249 converted to this mark on the server before coming to the web
250 front end. Defaults to `window.urb.bind.mark`, which defaults
251 to "json".
252- `wire`: wire of the subscription. Defaults to the given path.
254The callback function here just updates the data in the `clicks`
255div. A truly robust app would intelligently handle errors, of
256course.
258This app can be tested from the web by going to
259`/~~/home/pub/click` on your server. Usually, this is
260`ship-name.urbit.org/~~/home/pub/click`.
262Note that the app doesn't have anything web-specific in itself.
263As far as it knows, it's just receiving pokes and subscriptions.
264The javascript is fairly pure as well, sending and receiving json
265everywhere. The marks are the translation layer, and they're the
266only things that need to know how the hoon types map to json.
268**Exercise**:
270- Open the app in multiple tabs, click the button, and verify
271 that all the tabs stay in sink. Poke it manually from the
272 command line and verify the tabs are updated as well.