on c19bc7a help sign in
urbit.org/docs/tutorials/gall/web-apps.udon
254 lines 8.8 kB history raw
1:- :~ navhome/'/docs/'
2 navuptwo/'true'
3 next/'true'
4 sort/'5'
5 title/'Web apps'
6 ==
7;>
9# Backend
11We've done purely functional web pages as hook files, and we've
12done command line apps, so now let's interact with an app over
13the web. The fundamental concepts are no different than from the
14command line -- you send "poke" messages to apps and subscribe to
15data streams. The only difference is that you do it with
16javascript.
18Let's create a small web app that counts the number of times you
19poke it. There'll be a button to poke the app, and line of text
20below it saying how many times the app has been poked. We can
21update this by subscribing our page to the relevant path on its app.
23Save the following as `click.hoon` in the `/app` directory of your urbit's pier.
25```
26/? 314
27!:
28|%
29++ move [bone %diff %mark *]
30--
31!:
32|_ [hid=bowl:gall clicks=@]
33++ poke-click
34 |= click=%click
35 ~& [%poked +(clicks)]
36 :_ +>.$(clicks +(clicks))
37 %+ turn (prey:pubsub:userlib /the-path hid)
38 |=([o=bone *] [o %diff %clicks +(clicks)])
39++ peer-the-path
40 |= pax=path
41 [[[ost.hid %diff %clicks clicks] ~] +>.$]
42--
43```
45There's nothing really new here, except that we use a couple of
46new marks, `click` and `clicks`. When we get poked with a
47`click`, we increment the variable `clicks` in our state and tell
48all our subscribers the new value. When someone subscribes to us
49on the path `/the-path`, we immediately give them the current
50number of clicks.
52Let's take a look at the new marks. Save this as `/mar/click.hoon`:
54```
55|_ click=%click
56++ grab
57 |%
58 ++ noun |=(* %click)
59 ++ json
60 |= jon=^json
61 ?> =('click' (need (so:dejs-soft:format jon)))
62 %click
63 --
64--
65```
67The mark `click` has type `%click`, which means the only
68valid value is `%click`. We can convert from `noun` by just
69producing `%click`.
71We also convert from json by parsing a json string with `so:dejs-soft:format`,
72asserting the parsing succeeded with `need`, and asserting the
73result was 'click' with `?>`, which asserts that its first child
74is true.
76> Note the argument to `++json`is `jon=^json`. Why `^json`? `++json` shadows the type definition, so if we want to refer to the type, we have to prepend a `^`. This extends to multiple levels: `^^^foo` means the fourth most innermost instance of `foo`.
78> `dejs-soft:format` in `zuse` is a useful library for parsing complex json into hoon structures. In this case, the `:` between `dejs-soft` and `format` means 'inside of', because `dejs-soft` is an arm contained within the core `format`. Our case is actually simple enough that the `?>` line could have been `?> =([%s 'click'] jon)`.
80We can test this mark from the command line (don't forget to start your app with `|start %click`)
82```
83~fintud-macrep:dojo> &click %click
84%click
86~fintud-macrep:dojo> &click &json [%s 'click']
87%click
89~fintud-macrep:dojo> &click &json &mime [/text/json (as-octs:mimes:html '"click"')]
90%click
92~fintud-macrep:dojo> &click &json &mime [/text/json (as-octs:mimes:html '"clickety"')]
93/~fintud-macrep/home/0/mar/click:<[7 5].[8 11]>
94ford: casting %json to %click
95ford: cast %click
96```
98And save the following as `/mar/clicks.hoon`:
100```
101|_ clicks=@
102++ grab
103 |%
104 ++ noun @
105 --
106++ grow
107 |%
108 ++ json
109 (frond:enjs:format %clicks (numb:enjs:format clicks))
110 --
111--
112```
114`clicks` is just an atom. We convert to json by creating an
115object with a single field "clicks" with our value.
117> Be sure to check out section 3bD, JSON and XML, in zuse.hoon `frond:enjs:format` is just a function that takes a key-value pair and produces a JSON object with one element.
119```
120~fintud-macrep:dojo> &json &clicks 6
121[%o p={[p='clicks' q=[%n p=~.6]]}]
122```
124Now that we know the marks involved, take another look at the app
125above. Everything should be pretty straightforward. Let's poke
126the app from the command line.
128```
129~fintud-macrep:dojo> |start %click
130>=
131~fintud-macrep:dojo> :click &click %click
132[%poked 1]
133>=
134~fintud-macrep:dojo> :click &click %click
135[%poked 2]
136>=
137```
139*Exercise*:
141- Modify `:sink` from the subscriptions chapter to listen to `:click` and print out the subscription updates on the command line.
143# Frontend
145> Note: the instructions below are outdated and do not work in the current version of Urbit. Feel free to experiment though.
147That's all that's needed for the back end. The front end is just
148some "sail" html (Hoon markup for XML) and javascript. Here's `/web/pages/click.hoon`:
150```
151;html
152 ;head
153 ;script(type "text/javascript", src "//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js");
154 ;script(type "text/javascript", src "/~/at/lib/js/urb.js");
155 ;title: Clickety!
156 ==
157 ;body
158 ;div#cont
159 ;input#go(type "button", value "Poke!");
160 ;div#err(class "disabled");
161 ;div#clicks;
162 ==
163 ;script(type "text/javascript", src "/pages/click/click.js");
164 ==
165==
166```
168You should recognize the sail syntax from an earlier chapter.
169Aside from jquery, we also include `urb.js`, which is a framework
170for interacting with urbit.
172To view the frontend, point your browser at `ship-name.urbit.org/~~/pages/click` if you're on the live network. If you're running a fake galaxy, navigate to whatever port it's running on, which is usually: `https://localhost:8443/~~/pages/click`.
174We have a button labeled "Poke!" and a div with id `clicks` where
175we'll put the number of clicks. We also include a small
176javascript file where the client-side application logic can be
177found. It's in `/web/pages/click.js`:
179```
180$(function() {
181 var clicks, $go, $clicks, $err
183 $go = $('#go')
184 $clicks = $('#clicks')
185 $err = $('#err')
187 $go.on("click",
188 function() {
189 window.urb.send(
190 "click", {mark: "click-click"}
191 ,function(err,res) {
192 if(err)
193 return $err.text("There was an error. Sorry!")
194 if(res.data !== undefined &&
195 res.data.ok !== undefined &&
196 res.data.ok !== true)
197 $err.text(res.data.res)
198 else
199 console.log("poke succeeded");
200 })
201 })
203 window.urb.appl = "click"
204 window.urb.bind('/the-path',
205 function(err,dat) {
206 clicks = dat.data.clicks
207 $clicks.text(clicks)
208 }
209 )
210})
211```
213We set up two event handlers. When we click the button, we run
214`window.urb.send`, which sends a poke to the app specified as the first argument, in this case our :click app. The arguments
215are data, parameters, and the callback. The data is the specific
216data we want to send. The parameters are all optional, but here's a list
217of the available ones:
219- `ship`: target urbit. Defaults to `window.urb.ship`, which
220 defaults to the urbit which served the page.
221- `appl`: target app. Defaults to `window.urb.appl`, which
222 defaults to null.
223- `mark`: mark of the data. Defaults to `window.urb.send.mark`,
224 which defaults to "json".
225- `wire`: wire of the poke. Defaults to `/`.
227In our case, we specify only that the mark is "click".
229The callback function is called when we receive an
230acknowledgment. If there was an error, we put it in the `err`
231div that we defined above. Otherwise, we printf to the console a
232message saying the poke succeeded. As is common, we gray
233out the button when it is clicked. It is only reenabled when positive acknowledgement is received.
235Our second event handler is `window.urb.bind`, which is called immediately when the page is loaded, subscribing the page to the specified data stream on the app set with `urb.appl` (which is, in this case, :click). It also takes three arguments: path, parameters, and callback. The path is the path on the app to subscribe to. The parameters are all optional (indeed, we omit them entirely here), but are similar to those for `send`:
237- `ship`: target urbit. Defaults to `window.urb.ship`, which defaults to the urbit which served the page.
238- `appl`: target app. Defaults to `window.urb.appl`, which defaults to null.
239- `mark`: mark of expected data. Data of other marks is converted to this mark on the server before coming to the web front end. Defaults to `window.urb.bind.mark`, which defaults to "json".
240- `wire`: wire of the subscription. Defaults to the given path.
242The callback function here just updates the data in the `clicks`
243div. A truly robust app would intelligently handle errors, of
244course.
246Note that the app doesn't have anything web-specific in itself.
247As far as it knows, it's just receiving pokes and subscriptions.
248The javascript is fairly pure as well, sending and receiving json
249everywhere. The marks are the translation layer, and they're the
250only things that need to know how the hoon types map to json.
252*Exercise*:
254- Open the app in multiple tabs, click the button, and verify that all the tabs stay in sink. Poke it manually from the command line and verify the tabs are updated as well.