WritingNewViews.md 7.55 KB
Newer Older
Laurent Wouters's avatar
Laurent Wouters committed
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
# Writing new views for CubicWeb Linked Data Browser

The [CubicWeb Linked Data Browser]() relies on views to render RDF datasets.
By default, it only provides a single view that simply lists the RDF triples from the loaded dataset.
All other views are user-defined and dynamically discovered and loaded by the browser.
This document explains the basics of writing new views.

At its core, a view is simply a piece of Javascipt code (or any language that compiles to it) that implements a simple interface.
There are a few other requirements, as listed below:

* The view implementation must conform to the [`ViewImplementation`]() interface.
* The Javascript resource containing the implementation must contain an entrypoint for the browser.
* The view implementation must be referred to by a endpoint serving a descriptor of the implementation.

This document go through all these requirements and explain how to fulfill them.
In the end, the implementation of a view does not require any special behavior from the server it.
They are simply static files.

## Step 1 - Implement `ViewImplementation`

A library has been designed to help at the implementation of views.
It is available on npm as `@logilab/libview`.
This library is also used by the browser and therefore provides all the necessary APIs and interfaces.

First, add this library as a dependency in your project:

```sh
npm install @logilab/libview
```

The next step is to start the implementation of the main interface to provide: `ViewImplementation`.
This interface is located in the `implementation` namespace within `@logilab/libview`.
A good starting point is (in Typescript):

```ts
import { application, definition, implementation } from "@logilab/libview";
import * as $rdf from "rdflib";

class MyView implements implementation.ViewImplementation {

  descriptor: definition.ViewDescriptor;

  constructor() {
    this.descriptor = ...;
  }

  priorityFor(store: $rdf.Formula, target: application.Resource): number {
      return 0;
  }

  render(
    renderer: application.ViewRenderer,
    context: application.RenderingContext,
    target: application.Resource
  ): implementation.ViewRendering {
    return ...;
  }
}
```

This interface is basically three members:

* `descriptor` is a public attribute that must contain the descriptor (metadata for the view).
* `priorityFor` is a method that, given a RDF dataset and a target resource (URI) to be rendered, must return the self-evaluated priority of the view.
This value is used by the browser to select an appropriate view for the dataset.
A higher (positive) number indicates a higher priority.
A negative number can be return to indicate that the view *must not* be selected by the browser.
As of this writing, the browser primarily relies on these self-evaluation by the views, unless the user explicitely select a particular view.
* `render` is the single entrypoint for the view implementation used by the browser. Given a rendered, a contact and a target resource (URI) to be rendered, it must return a rendering that may or may not include an HTML DOM sub-tree.

The basic idea for a view implementation is for the browser to specify the RDF dataset and the main resource to render and for the view to provide an HTML DOM tree.
How the DOM tree is produced is left to the discretion of the view implementation.
Any framework may be used, such as React, Angular, Vue, etc.; or no framework at all.

### Descriptor

The view descriptor is an object that conform the `ViewDescriptor` interface.
It *must* have the following fields:

* `identifier`, a unique identifier for the view.
* `name`, a short human-readable name for the view.
* `description`, a more lengthier description for the view. It may expands of the kind of RDF data that the view is able to handle.
* `entrypoint`, the name of an exported variable that contains an instance of the view implemention. See Step 2.
* `resourceCss`, an array of potentially remote CSS resources that should be loaded by the browser for this view.
* `resourceJs`, an array of potentially remote additional Javascript resources that should be loaded by the browser for this view.
* `resourceMain`, points to the potentially remote Javascript resource that contains the view implementation.

Example:

```ts
const MY_VIEW_DESCRIPTOR: definition.ViewDescriptor = {
  identifier: "::SeriousBusiness::MyView",
  name: "A short human-readable name",
  description: "A lengthier description of this view. What kind of RDF data does it handle?",
  entrypoint: "VIEW_ENTRYPOINT", // name of the entrypoint object
  resourceCss: [
    {
      location: "remote",
      uri:
        "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
    }
  ],
  resourceJs: [],
  resourceMain: {
    location: "remote",
    uri: "http://views.example.com/myview.js"
  }
};
```

### Priority evaluation

The RDF dataset.

The target RDF resource.


### Rendering

The renderer.

The rendering context.

The resulting rendering.

### Facilities to manipulate RDF data

RDF entities and entity store.

Load objects from RDF data.


## Step 2 - Entrypoint

When the resource (file) in which a view implementation is defined is loaded by the browser, it needs to known what is the main object that represents the actual view implementation.
This entity is the entrypoint.
It usually is a simple constant that holds an instance of the view implementation object that implements the `ViewImplementation` interface.
Continuing on the example, the entrypoint may be defined as follow:

```ts
export const VIEW_ENTRYPOINT: MyView = new MyView();
```

The name of the entrypoint, `VIEW_ENTRYPOINT` in the excerpt above, *must* be the same as the name given in the view descriptor.


## Step 3 - Serve the view

The last step consists in setting-up a JSON file containing a list of one or more view descriptors.
The descriptors in the file *must be* identical to the ones provided by the view implementations themselves.
For example:

```json
[
  {
    "identifier": "::SeriousBusiness::MyView",
    "name": "A short human-readable name",
    "description": "A lengthier description of this view. What kind of RDF data does it handle?",
    "entrypoint": "VIEW_ENTRYPOINT",
    "resourceCss": [
      {
        "location": "remote",
        "uri": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
      }
    ],
    "resourceJs": [],
    "resourceMain": {
      "location": "remote",
      "uri": "http://views.example.com/myview.js"
    }
  },
]
```

In this descriptor, the uri provided for the main resource *must* point to the Javascript file that contains the view implementation.
The JSON file can then be served and the browser can points to it as a source of view so that the implementations can be loaded.

The `@logilab/libview` library contains a ready-to-use `index.html` that can be used as a companion to the JSON file to provide a more human-readable landing page for users.
A working setup would be to serve the following directory at an URI like `http://views.example.com/`:

```
<root>
  +-> index.html      copied from @logilab/libview
  +-> index.vd.json   JSON file that contains the view descriptors
  +-> myview.js       Javascript resource containing the implemenation of MyView
```

The process of copying the `index.html` file can be automated, for example by using webpack in `webpack.config.js`:

```js
module.exports = [
  {
    plugins: [
      new CopyWebpackPlugin(
        [
          { from: "src/index.vd.json", to: "index.vd.json" },
          {
            from: "node_modules/@logilab/libview/build/index.html",
            to: "index.html"
          }
        ],
        {}
      )
    ]
  }
];
```