-
Notifications
You must be signed in to change notification settings - Fork 146
/
webapp-intro.html
630 lines (508 loc) · 26.5 KB
/
webapp-intro.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
<html>
<head>
<title>rdflib: Introduction for building web apps</title>
<style type="text/css">
body {
font-family: Trebuchet MS, Palatino, sans-serif;
color: black;
background: white;
}
p , ul, ol { text-indent : 0em ;
margin-left: 3em ; /* a bit of white space */ }
pre { margin-left: 5em; background-color: #eee; padding: 0.5em;}
li { text-indent: 0; }
h1 { text-align: center }
h2 { font-style: bold; margin-left: 1em; }
h3 { font-style: bold; margin-left: 3em; }
h4 { font-style: italic; margin-left: 3em; }
address { text-align: right }
a:link, a:active { color: #00e; background: transparent; text-decoration: none; }
a:visited {color: #529; background: transparent;}
div.intro {margin-left: 5%; margin-right: 5%; font-style: italic}
pre { font-family: monospace }
a:link img, a:visited img { border-style: none }
UL.toc { list-style: disc; list-style: none;}
div.issue { padding: 0.5em; border: none; margin-right: 5%; }
table: { border: collapse; }
td: { background-color: #eee; }
</style>
</head>
<body>
<h2>What is rdflib?</h2>
<p>The easiest and best way to work with linked data in Solid is to use a library called rdflib. Rdflib is a general toolbox for doing most things for linked data. It can store data, parse and serialize data into various formats, and keep track of changes to the data coming from the app or from the server.</p>
<h3>Glossary of Terms</h3>
<p>Here are some basic vocabulary terms we'll be using throughout this document.</p>
<ul><li><b>Store</b> - data structure to store graph data and perform queries against. This is the simplest way to work with linked data in rdflib. You can store data from Javascript, dump data out from it, or perform raw queries.</li>
<li><b>Fetcher</b> - A helper object that connects to the web, loads data, and saves it back. More powerful than using a simple store object. When you have a fetcher, then you also can ask the query engine to go fetch new linked data automatically as your query makes its way across the web.</li>
<li><b>UpdateManager</b> - An even more helper object. The UpdateManager allows you to send small changes to the server to “patch” the data as your user changes data in real time. It also allows you to subscribe to changes other people make to the same file, keeping track of upstream and downstream changes, and signaling any conflict between them.</li>
<li><b>Graph</b> - A database for the semantic web. This database is seemingly arbitrary in terms of what is related to what. There are no parent or root nodes, and the connections between nodes is key.</li>
<li><b>Triples </b>- An RDF concept that comprise of subject, predicate, and object. For example, storing the data “I have the name John” would be represented as a triple. Similarly,</li>
<li><b>Quad</b> is like a triple, but also has a property to explain where the data came from.</li>
<li><b>Statement</b> - Another word for quad.</li>
</ul><h3>Setting up rdflib.js</h3>
<p>Typically people define rdflib in your module as $rdf, so that you an easily cut and paste code between projects and examples here, without confusion.</p>
<p>Installation steps (using npm):</p>
<pre>
<code class="language-javascript">npm install rdflib --save
</code></pre>
<p>and then in your code, you will need the following line as well:</p>
<pre>
<code class="language-javascript">const $rdf = require(‘rdflib’)
</code></pre>
<h3>Setting up a Store</h3>
<p>Suppose we have a store, and we set up a person and their profile. Their webid is the URI '<a href="https://example.com/alice/card#me">https://example.com/alice/card#me</a>', which is, if you like, a local variable ‘me’ within the the file '<a href="https://example.com/alice/card">https://example.com/alice/card</a>'. </p>
<p>There are two ways of creating a store: </p>
<pre>
<code class="language-javascript">const store = new $rdf.IndexedFormula
</code></pre>
<p>and the shortcut:</p>
<pre>
<code>const store = $rdf.graph();
</code></pre>
<h3>Using the Store</h3>
<p>Let's set up a variable for the person of interest, and one for their profile document. Note that the URIs for abstract things in RDF have a # and a local id, just like anchors in HTML. The NamedNode method doc() generates a Named Node for the document.</p>
<pre>
<code>const me = store.sym('https://example.com/alice/card#me');
const profile = me.doc(); //i.e. store.sym(''https://example.com/alice/card#me')</code></pre>
<p>We are going to be using the VCARD terms, and so we set up a <b>Namespace</b> object to which generates the right predicate URIs for each term.</p>
<pre>
<code class="language-javascript">const VCARD = new $rdf.Namespace(‘http://www.w3.org/2006/vcard/ns#‘);
</code></pre>
<p>If we don’t know which vocabulary to use, various groups have their favorite lists. One is the <a href="https://github.com/solid/solid-namespace">collection of RDF namespaces in Solid projects</a>.</p>
<p>We add a name to the store as though it was stored in the profile</p>
<pre>
<code class="language-javascript">store.add(me, VCARD(‘fn’), “John Bloggs”, profile);
</code></pre>
<p>The third parameter, the object, is formally an RDF Term, here it would be a <strong>Literal</strong>. But you can give a string like “John Bloggs”, and rdflib will generate the right internal object. It will do that with strings, numbers, Javascript Date objects. </p>
<p>We have some data - one quad - in our store! Let's read it out.</p>
<p>Now to check what name is given to this person <i>specifically</i> in their profile, we do:</p>
<pre>
<code class="language-javascript">let name = store.any(me, VCARD('name'), null, profile);
</code></pre>
<p>If you are <i>not</i> concerned which file the data may have come from, then you can omit the last parameter -- in fact the object too as it is a wildcard:</p>
<pre>
<code class="language-javascript">let name = store.any(me, VCARD('name'));
</code></pre>
<p>Then you will pull in any name from any file you have loaded.</p>
<p>So we have added triples here to a local store. That has just been using it as an in-memory database. Most of the time in a Solid app, we’ll use it as a way of getting and saving data to the web. </p>
<h4>Using the Store with Turtle</h4>
<p>Let’s look at two more local operations. If you have turtle text for some data, you can load it into the store using $rdf.parse:</p>
<pre>
<code class="language-javascript">let text = '<#this> a <#Example> .';
let doc = $rdf.sym(‘’https://example.com/alice/card”);
let store = $rdf.graph();
$rdf.parse(text, store, doc.uri, ‘text/turtle’); // pass base URI
</code></pre>
<p>Note that we must specify a document URI, as the store works by keeping track of where each triple belongs. </p>
<pre>
<code>> store.toNT()
'{<https://example.com/alice/card.ttl#this> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://example.com/alice/card.ttl#Example> .}'
</code></pre>
<p>We can similarly generate a turtle text from the store. Serialize is the function. You pass it the document (as a NamedNode) we are talking about, and it will just select the triples from that document to be output.</p>
<pre>
<code class="language-javascript">console.log($rdf.serialize(doc, store, aclDoc.uri, 'text/turtle'));
</code></pre>
<p>If you omit the document parameter to serialize, or pass null, then you will get all the triples in the store. This may, if you have used a Fetcher, possibly metadata which the fetcher has stored about the HTTP requests it made in fetching your documents. Which might be interesting... but not what you were expecting.</p>
<h4>Using match() to Search the store</h4>
<p>The store’s match(s, p, o, d) method allows you to pull out any combination of quads:</p>
<pre>
<code class="language-javascript">let quads = store.match(subject, predicate, object, document);
</code></pre>
<p>Any of the parameters can be null (or undefined) as a wildcard, meaning “any”. The quads which are returned are returned as an array Statement objects.</p>
<p>Examples:</p>
<table><colgroup><col width="*" /><col width="*" /></colgroup><tbody><tr><td>
<p dir="ltr">match()</p>
</td>
<td>
<p dir="ltr">gives all the statements in the store</p>
</td>
</tr><tr><td>
<p dir="ltr">match(null, null, null, doc)</p>
</td>
<td>
<p dir="ltr">gives all the statements in the document</p>
</td>
</tr><tr><td>
<p dir="ltr">match(me, null, null, me.doc())</p>
</td>
<td>
<p dir="ltr">gives all the statements in my profile where I am the subject</p>
</td>
</tr><tr><td>
<p dir="ltr">match(null, null, me, me.doc())</p>
</td>
<td>
<p dir="ltr">gives all the statements in my profile where I am the object</p>
</td>
</tr><tr><td>
<p dir="ltr">match(null, LDP(‘contains’))</p>
</td>
<td>
<p dir="ltr">gives all the statements whose predicate is ldp:contains</p>
</td>
</tr></tbody></table><p>Once you have a set of statements, you typically want to look at the properties of the statement.</p>
<table><colgroup><col width="*" /><col width="*" /></colgroup><tbody><tr><td>
<p dir="ltr">subject</p>
</td>
<td>
<p dir="ltr">The Node of the subject</p>
</td>
</tr><tr><td>
<p dir="ltr">predicate</p>
</td>
<td>
<p dir="ltr">The NamedNode of the predicate</p>
</td>
</tr><tr><td>
<p dir="ltr">object</p>
</td>
<td>
<p dir="ltr">The Node of the subject</p>
</td>
</tr><tr><td>
<p dir="ltr">why</p>
</td>
<td>
<p dir="ltr">The NamedNode of the document</p>
</td>
</tr></tbody></table><p>The last property is called <i>why</i> because it tells us why we should believe it. In a simple linked data system this is a document we have read. In a more complex system this could point to an inference step. It can also be a special object put in by the Fetcher to store the results of its HTTP operations on the web.</p>
<p>So to find out all the document which mention an old email address as the object of any statement</p>
<pre>
<code class="language-javascript">
let oldEmail = $rdf.sym('mailto:albert@example.com')
let outOfDate = store.match(null, null, oldEmail, null).map(st => st.why);
</code></pre>
<p>Note how we pull in all the statements and then just keep the ‘why’ parts.</p>
<p>So to find out all the document which mention Alice as the subject or object of any statement</p>
<pre>
<code class="language-javascript">
let mentions = store.match(alice, null, null, null).concat(store.match(null, null, alice, null)).map(st => st.why);
</code></pre>
<p>Note also while we are here the handy<p>
<pre>
<code class="language-javascript">
let aboutAlice = store.connectedStatements(alice, alice.doc())
</code></pre>
<p>which pulls in the statements which mention Alice, plus those which mention connected blank nodes,
which could include things like the structure of Alice's address, for example.</p>
<p>Suppose we have loaded a bunch of LDP folders and we want to pull out all the pairs of files where one is inside the other.</p>
<pre>
<code class="language-javascript">store.match(null, LDP(‘contains’)).forEach(st => {
console.log(st.subject + ‘ contains + st.object)
});
</code></pre>
<p><i>We have introduced you to match() after the methods any() and each() because most of the time when you are programming we find those are actually more convenient than using match.</i></p>
<h4>Making new Statements</h4>
<p>You can make a new statement using:</p>
<pre>
<code class="language-javascript">let st = new $rdf.Statement(me, FOAF(‘name’), “Joe Bloggs”, me.doc());
</code></pre>
<p>or if that's too verbose, you can use a shortcut provided:</p>
<pre>
<code class="language-javascript">let st = $rdf.st(me, FOAF(‘name’), “Joe Bloggs”, me.doc());
</code></pre>
<p>The "st" shortcut exists because you can pass arrays of statements to be deleted or inserted to the UpdateManager's "update()" function as a convenient way of making small changes to the web of data.</p>
<p>But before we get into using the UpdateManager, let's look at the Fetcher, which is your first level of connection to the web.</p>
<h2>Using the Fetcher</h2>
<p>The Fetcher is a "helper object" which you can attach to a store
to allow it to connect to the read-write web of data.
The Fetcher handles the HTTP requests, understands MIME types and different
formats. It uses the 4th column of the quadstore to track where each triple came from.
It can parse data from the net (or elsewhere) and put it in the store.
It can generate pretty-printed formatted data from the store, the whole store,
or the data corresponding to one data document out there.
</p>
<img src="https://raw.githubusercontent.com/linkeddata/rdflib.js/Documentation/diagrams/rdflib_block_diagram.png" />
<p>Let's set up a store as before.</p>
<pre>
<code class="language-javascript">const store = $rdf.graph();
const me = store.sym('https://example.com/alice/card#me');
const profile = me.doc() // i.e. store.sym(''https://example.com/alice/card#me');
const VCARD = new $rdf.Namespace(‘http://www.w3.org/2006/vcard/ns#‘);
</code></pre>
<p>This time, we'll also make a Fetcher for the store. The Fetcher is a helper object which allows you to transfer data to and from the web of data. </p>
<pre>
<code class="language-javascript">const fetcher =new $rdf.Fetcher(store);
</code></pre>
<p>Now let's load the document we have been talking about.</p>
<pre>
<code class="language-javascript">fetcher.load(profile).then(response => {
let name = store.any(me, VCARD(‘fn’));
console.log(`Loaded {$name || ‘wot no name?’}`);
}, err => {
console.log(“Load failed “ + err);
});
</code></pre>
<p>Typically when dealing with people, a name and an avatar is useful in the user interface. Let's pick up the picture too, but also make the code a little more robust against people having profiles written using different terms.</p>
<pre>
<code class="language-javascript">const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
</code></pre>
<p>This way, we can try using this namespace if there is no VCARD name.</p>
<pre>
<code class="language-javascript">let name = store.any(me, VCARD(‘fn’)) || store.any(me, FOAF(‘name’));
let picture = store.any(me, VCARD(‘hasPhoto’)) || store.any(me, FOAF(image));
</code></pre>
<p>Or we can track all the names we find instead. The function "each()" returns an array of any field it finds a value for.</p>
<pre>
<code class="language-javascript">let names = store.each(me, VCARD(‘fn’)).concat(store.each(me, FOAF(‘name’)));
</code></pre>
<h4>Fetch Full Code Example</h4>
<p>Let’s build a little card for someone and set it to get a picture from the net when it can. Let’s use the raw DOM as for the sake of an example -- you translate this into the equivalent in your favorite UI framework.</p>
<pre>
<code class="language-javascript">const store = $rdf.graph();
const fetcher = new $rdf.Fetcher(store);
const me = store.sym('https://example.com/alice/card#me')
const VCARD = new $rdf.Namespace(‘http://www.w3.org/2006/vcard/ns#‘);
const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
function cardFor (person) {
let div = document.createElement(div);
div.outerHTML = `<div style = ‘padding: 0.5em;’>
<img style = ‘max-width: 3em; min-width: 3em; border-radius: 0.6em;’
src = ‘@@default person image from github.io’>
<span style=’text-align: center;’>???</span>
</div>
`;
let image = div.children[0];
let span = div.children[1];
store.load(person).then( response => {
let name = store.any(person, VCARD(‘fn’));
if (name) {
label.textContent = name.value; // name is a Literal object
}
let pic = store.any(person, VCARD(‘hasPhoto’));
if (pic) {
image.setAttribute(‘src’, pic.uri); // pic is a NamedNode
}
});
return div;
}
</code></pre>
<p>Then inside of our web application, we could run the following commands:</p>
<pre>
<code class="language-javascript">div.appendChild(card(me)); // My card
fetcher.load(me.doc).then(resp -> {
store.each(me, FOAF(‘friend’)).forEach(friend => div.appendChild(card(friend)));
});
</code></pre>
<p>This will pull in the user’s profile to make the picture and name for my own card. Then we explicitly pull it in again to find the list of friends. This is reasonable as the fetcher’s `load` method really means “load if you haven’t already” so continues immediately if it has already been fetched. It’s wise to explicitly load the things you need and let the system track what has already been loaded.</p>
<p>Then for each of the friends it will load their profile to fill in the name and picture of the friend.</p>
<p><i>Tip: if you are doing this in the Solid world it is good to make any representation of a thing </i><b><i>draggable</i></b><i> with the URI of the thing as the dragged URI. That means users of your UI will be able to drag say, people from your window into another solid app, to say add them to a group, give them access to things, and so on. Similarly, if your window real estate would be a logical place for users to drop other things or people, make it a drag target. For devices with drag and drop anyway.</i></p>
<h3>Listing Data</h3>
<p>Everything in RDF is a thing. We store data about all things in the same sort of way, just using different vocabulary. Suppose you want to list the content of the folder in someone’s solid space. It is very like listing their friends. The namespace for the contents of folders is LDP. So..</p>
<pre>
<code class="language-javascript">const LDP = $rdf.Namespace(‘http://www.w3.org/ns/ldp#>’);
let folder = $rdf.sym(‘https://alice.example.com/Public/’); // NOTE: Ends in a slash
fetcher.load(folder).then(() => {
let files = store.any(folder, LDP(‘contains’));
files.forEach(file) {
console.log(‘ contains ‘ + file);
}
});
</code></pre>
<p>The Solid pods give you a bit of metadata about each contained file, including size and type. For example we can look at the RDF type ldp:Container to see when we can list something as a subfolder:</p>
<pre>
<code class="language-javascript">function list(folder, indent) {
indent = indent || ‘’;
fetcher.load(folder).then(() => {
let files = store.any(folder, LDP(‘contains’));
files.forEach(file) {
console.log(indent + folder + ‘ contains ‘ + file);
if (store.holds(file, RDF(‘type’), LDP(‘Container’)) {
list(file, indent + ‘ ‘);
}
}
});
}
list(rdf.sym(‘https://alice.example.com/Public/’));</code></pre>
<p>The results will come asynchronously. If we were building a UI, each would get slotted into the right place.</p>
<h2>Update: Using UpdateManager to update the web</h2>
<p>The UpdateManager is another helper object for the store. Just as the Fetcher
allows the store to read and write resources from the web, generally a resource (file) at a time,
the UpdateManager object allows the store to send small changes to the data web.
It also allows the web app to subscribe to a stream of changes that other people have made,
and so keep all places where the data is displayed in sync.
</p>
<pre>
const store = $rdf.graph()
const fetcher = new $rdf.Fetcher(store)
const updater = new $rdf.UpdateManager(store)
...
function setName(person, name, doc) {
let ins = $rdf.st(person, VCARD('fn'), name, doc)
let del = []
updater.update(del, ins, (uri, ok, message) => {
if (ok) console.log('Name changed to '+ name)
else alert(message)
})
}
</pre>
<p>
The first parameter to update() is the array of statements to be deleted.
If it is empty then update() just adds data to the store.
(Note for this the user only needs Append privileges, not full Write).
The second parameter to update() is the array of statements to be deleted.
</p>
<pre>
function modifyName(person, name, doc) {
let ins = $rdf.st(person, VCARD('fn'), name, doc)
let del = store.statementsMatching(person, VCARD('fn'), null, doc) // null is wildcard
updater.update(del, ins, (uri, ok, message, response) => {
if (ok) console.log('Name changed to '+ name)
else alert(message)
})
}
</pre>
<p>So in this second case, the function will first find any statements which
give the name of the person. It then asked update to in one operation
remove the old statements (quads) from the store, and add the new one.
</p>
<h4>409 Conflict</h4>
<p>Note that update operation (which does a PATCH operation) is specified by solid
to be atomic, in that it will either complete both deletion and insertion, or
it will fail and do nothing. If the server is not able to delete the statements,
for example because someone else has just deleted them first, then the update
must fail with a <b>409 conflict</b>. In that case, the web app will typically
beep or turn pink and back out the user's attempted change in the UI.
</p>
<h2>Deleting resources</h2>
<p>To delete triples, or any combination of them, from a resource, use the UpdateManager above. If you
want to delete whole resources, then you use the HTTP <tt>DELETE</tt> method.</p>
<pre>
store.fetcher.webOperation('DELETE', doc.uri).then(...)
</pre>
<h4>Example: recursive delete of Solid folders</h4>
<p>Like in Unix, you can't (currently, 2018) delete a folder unless it is empty.
So if you want to, you have to delete everything in it first.
Here is your <tt>rm -r</tt> function to complete this little guide. </p>
<pre>
function deleteRecursive (store, folder) {
return new Promise(function (resolve, reject) {
store.fetcher.load(folder).then(function () {
let promises = store.each(folder, ns.ldp('contains')).map(file => {
if (store.holds(file, ns.rdf('type'), ns.ldp('BasicContainer'))) {
return deleteRecursive(kb, file)
} else {
console.log('deleteRecursive file: ' + file)
if (!confirm(' Really DELETE File ' + file)) {
throw new Error('User aborted delete file')
}
return store.fetcher.webOperation('DELETE', file.uri)
}
})
console.log('deleteRecursive folder: ' + folder)
if (!confirm(' Really DELETE folder ' + folder)) {
throw new Error('User aborted delete file')
}
promises.push(store.fetcher.webOperation('DELETE', folder.uri))
Promise.all(promises).then(res => { resolve() })
})
})
}
</pre>
<p>Use with care.. </p>
<h2>Tracking Changes</h2>
<p>Using the UpdateManager above we made an app which sends changes to the data web whenever the UI changes.
Let's also make it so the UI changes whenever the data web changes. The function we use is:
</p>
<pre>
updater.addDownstreamChangeListener(doc, refreshFunction)
</pre>
<p>
It will sign up for changes by opening a websocket with the server.
When a message comes from the server that the document has changed, it will reload the document into the store.
It will deal with you calling it for the same document from different places.
It will ignore changes which you have made yourself with updater.update().
So all you have to do is provide a function which will sync changes in the store into the UI.
</p>
<p>
If the user isn't editing the UI, just looking at it, you can more or less get away with
</p>
<pre>
const div = dom.createElement(div')
function refresh () { // Not recommended
div.innerHTML = ''
store.each(subject, predicate, null, doc).forEach(obj) {
div.appendChild(renderOneObject(obj))
}
}
refresh()
updater.addDownstreamChangeListener(doc, refresh)
</pre>
<p> or words to that effect. This will cause the div to be repainted.
That isn't as slick as writing a refresh function which adjusts the UI
just deleting old things and inserting new ones. That means the user can happily be
looking at or editing one part while other parts change.
</p>
<pre>
mugshotDiv = div.appendChild(dom.createElement('div'))
function elementForImage (image) {
let img = dom.createElement('img')
img.setAttribute('style', 'max-height: 10em; border-radius: 1em; margin: 0.7em;')
img.setAttribute('src', image.uri)
return img
}
function syncMugshots () {
let images = kb.each(subject, ns.vcard('hasPhoto'), null, doc)
images.sort() // arbitrary consistency
images = images.slice(0, 5) // max number for the space
UI.utils.syncTableToArray(mugshotDiv, images, elementForImage)
}
syncMugshots()
updater.addDownstreamChangeListener(doc, syncMugshots)
</pre>
<p>This uses a handy function syncTableToArray, which comes with solid-ui, but is include here to
be complete. Also, to encourage you to make UI which syncs in both directions, even if it isn't
built into the framework you are using. So find a way that rdflib fits naturally
with your favorite UI framework, if you have one. See
<a href="https://solid.inrupt.com/docs">the Inrupt Solid documentation</a> for some hints about using
specific frameworks.
</p>
<pre>
function syncTableToArray (table, things, createNewRow) {
let foundOne, row, i
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
row.trashMe = true
}
for (let g = 0; g < things.length; g++) {
var thing = things[g]
foundOne = false
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
if (row.subject && row.subject.equals(thing)) {
row.trashMe = false
foundOne = true
break
}
}
if (!foundOne) {
let newRow = createNewRow(thing)
// Insert new row in position g in the table to match array
if (g >= table.children.length) {
table.appendChild(newRow)
} else {
let ele = table.children[g]
table.insertBefore(newRow, ele)
}
newRow.subject = thing
} // if not foundOne
} // loop g
for (i = 0; i < table.children.length; i++) {
row = table.children[i]
if (row.trashMe) {
table.removeChild(row)
}
}
} // syncTableToArray
</pre>
<h2>Conclusion</h2>
<p>
We've looked at how to interact directly with the store as a local in-memory triple store, and we
have looked at how to load it and save it, web resource at a time.
We see how it ina away acts as a local in-memory cache of the big wide web of linked data,
allowing a user-interface to keep in sync with the state of the data web.
As developers we can now write code which will allow users to explore, create, modify and link together
a web of linked data which can grow to encompass more and more domains, different applications.
</p>
</body>
</html>