docs: add docs on component and data config (#406)

This commit is contained in:
Miralem Drek
2020-04-15 06:43:08 +02:00
committed by GitHub
parent cfa7ac2fb3
commit 1f30488c64
13 changed files with 730 additions and 14 deletions

BIN
docs/assets/hub-app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
docs/assets/hub-connect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
docs/assets/hub-dev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

142
docs/sn-component.md Normal file
View File

@@ -0,0 +1,142 @@
---
id: sn-component
title: Rendering
---
The `component` portion of the definition is where all rendering takes place, it's just a function that does not return anything:
```js
export default function () {
return {
component() {
// rendering logic goes here
},
};
}
```
In order to render something you need to access the DOM element the supernova is assigned to, you can do so by importing the `useElement` function:
```js
import { useElement } from '@nebula.js/supernova';
```
This function returns a simple HTMLElement which is your entry point to the visual world:
```js
component() {
const element = useElement();
element.innerHTML = 'Hello!';
}
```
`useElement` is one of many functions that provide you with the most common requirements when developing a chart, they allow you to _hook_ into the resources provided by both `nebula.js` and Qlik's Associative Engine.
## Hooks
If you have been working with [React](https://reactjs.org/) you might recognize this as _hooks_. Hooks is a concept which emphasizes reusable composable functions rather than classical object oriented classes and inheritance. While our implementation is completely custom with our own hooks, the concept and rules are very similar, so much so that you can read the [React hooks documentation](https://reactjs.org/docs/hooks-intro.html) to understand how to use nebula's own hooks.
### useElement
You've already seen the `useElement` hook, it's only purpose is to provide the HTMLElement you need to attach your own elements to to make your content visible, in the following example the element's `innerHTML` is set to `Hello!`:
```js
component() {
// get the element
const element = useElement();
// set element content
element.innerHTML = 'Hello!';
}
```
A static string won't accomplish much though, in most cases you will be updating the content based on data input, component state and user interactions.
The `component()` function is executed every time something that might be connected to your rendering changes; theme, data model, data selections, component state etc. As such, adding and removing event listeners, updating DOM nodes and fetching data is not ideal and can be quite performance heavy if done every time `component()` is run. You should instead batch updates with `useEffect`.
### useEffect
`useEffect` is a hook that accepts a callback function which will be run only when the value you specify changes. This enables you to not only batch updates but to also implement your own form of lifecycle management in your component.
```js
import { useEffect } from '@nebula.js/supernova';
// ...
component() {
const element = useElement();
useEffect(() => {
// run only once when the component is created
console.log('created');
}, []);
}
```
Adding event listeners to the element is typically only done when the component is initiated, and then removed when the component is destroyed:
```js
component() {
const element = useElement();
useEffect(() => {
const listener = () => {
console.log('clicked');
};
element.addEventListener('click', listener);
return () => {
// clean-up
element.removeEventListener('click', listener);
};
}, [element]);
}
```
In the example above, `element` is provided as an observable value as the second argument so the effect will only run when `element` changes. However, since `element` never changes for the same component the callback will only run once when the component is created. So `listener` will only be instantiated once and only one `click` event listener will be added. The callback also returns a function in the end, it's a clean-up function that is executed when any of the observable values change, or when the component is destroyed. This is where you should clean-up any sideffects you added, in this case the event listener is removed to avoid a memory leak.
### useState
Since `component()` is a function and not a class or object instance, you can not use `this` to store instance values as you would otherwise. The way to store state is through `useState`:
```js
import { useState } from '@nebula.js/supernova';
export default function () {
return {
component() {
const [count, setCount] = useState(0);
},
};
}
```
`useState` returns a tuple where the first item is the same as the initial value provided as argument to `useState`, while the second item is a setter function through which you can change the value.
In the following example `count` is incremented by 1 when a user clicks on `element`:
```js
component() {
const element = useElement();
const [count, setCount] = useState(0);
useEffect(() => {
const listener = () => {
setCount(count + 1);
};
element.addEventListener('click', listener);
return () => {
element.removeEventListener('click', listener);
};
}, [element]);
}
```
To render the updated value you can add another `useEffect` that is run when `count` changes:
```js
const [count, setCount] = useState(0);
useEffect(() => {
element.innerHTML = `Count: ${count}`;
}, [count]);
```

155
docs/sn-configure-data.md Normal file
View File

@@ -0,0 +1,155 @@
---
id: sn-configure-data
title: Configuring data
---
The `qae` section of the supernova definition is where you define the properties of the Generic Object and the shape of the data you expect to consume.
```json
{
"qae": {
"properties": {}
}
}
```
## Generic Object
Every supernova is connected to the entire data model and Qlik's Associative Engine through a Generic Object. This is a JSON object containing _properties_ which result in a _layout_ that describe the state of the backend portion of your supernova.
Every time someone wants to render your supernova, an instance of the generic object will be created in the data model. If the creator has the right permissions, they can choose to store and persist this object in their data model.
### Properties
What properties you set is entirely up to you, it must however be a valid JSON object. Most properties are just settings that you may want to persist over time. If you are developing a bar chart you might want to store a setting that indicates whether it should be stacked, or what color the bars should have:
```json
{
"isStacked": true,
"barColor": "red"
}
```
These are static properties only, what goes in comes out exactly the same. The true power of the Generic Object are the dynamic properties you can set that enables you to leverage Qlik's Associative Engine and access the data inside it.
Dynamic properties have a specific structure that enables the backend to differentiate between dynamic properties and static. They also have a naming convention where they all begin with a `q` followed by a capital letter, this makes it easy for both humans and machines to distinguish between the two property types.
There are a lot of different [predefined](https://core.qlik.com/services/qix-engine/apis/qix/definitions/) dynamic properties for various purposes, you can for example use [ValueExpression](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#valueexpression) to do simple calculations:
```json
{
"simpleMath": {
"qValueExpression": {
"qExpr": "1+1"
}
}
}
```
### Layout
The _layout_ of the Generic Object is the result of the static and dynamic properties. The layout of the properties above will look like this:
```json
{
"isStacked": true,
"barColor": "red",
"simpleMath": 2
}
```
Notice that the static properties remain exactly the same, while the dynamic property `simpleMath` now contains the calculated result of the ValueExpression.
Most dynamic properties have an input definition and a corresponding layout output; a [ListObjectDef](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#listobjectdef) will result in a [ListObject](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#listobject), a [SelectionObjectDef](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#selectionobjectdef) will result in a [SelectionObject](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#selectionobject), and so on.
## Data
The most common property definition you will be using is the [HyperCubeDef](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#hypercubedef), this is the dynamic property that will provide you with data from the backend data model. You can place this in the root of the properties object, or on a deeper level, and you can have as many as you need:
```json
{
"qHyperCubeDef": {},
"anotherOne": {
"qHyperCubeDef": {},
"andAThird": {
"qHyperCubeDef": {}
}
}
}
```
The primary input to the HyperCubeDef are _dimensions_ and _measures_:
```json
{
"qHyperCubeDef": {
"qDimensions": [{ "qLibraryId": "hdg534" }],
"qMeasures": [{ "qLibraryId": "gt5dgd" }]
}
}
```
In this case the dimensions and measures are hardcoded to a predefined value that might exist in a specific data model, which is rarely what you want. If you are developing a chart for others to use with any data model you need to make it possible to add those dynamically, you can do this be specifying _data targets_.
### Data targets
A data target is a way for you to define where the dynamic HyperCubeDefs are located in the Generic Object's properties. While `nebula.js` could traverse the properties and locate all usages of `qHyperCubeDef`, you may not want all of those to be dynamic, or you may generate them for internal use only.
You specify data targets with the `data.targets` key in `qae`, each target must have a `path` key which indicates the JSON path of the HyperCubeDef from the root of the properties object:
```js
qae: {
properties: {
qHyperCubeDef: {},
my: {
nested: {
qHyperCubeDef: {}
},
}
},
data: {
targets: [
{ path: '/qHyperCubeDef' },
{ path: '/my/nested/qHyperCubeDef' },
];
}
}
```
You can for each data target specify additional details like the maximum/minimum amount of dimensions and measures, and make modifications when they are added.
This is useful when you now the limitations of what a chart can render, a pie chart for example is mostly usable when it has exactly one dimension and one measure, but you might also be implementing support for a second measure. This also saves you some code since `nebula.js` won't attempt to render a chart whose limitations have not been fulfilled, and will instead show that some fields are missing.
### Field limitations
To specify limitations on dimensions or measures, add each respective field type as an object as part of the data target:
```js
targets: [
{
path: '/qHyperCubeDef',
dimensions: {
min: 1,
max: 1,
},
measures: {
min: 1,
max: 2,
},
},
];
```
### Field modifications
You can also modify an added or removed field just before the change is applied and sent to the backend, this is useful for things like setting sorting when a dimension is added, or adding additional properties that you know you will always need.
If you for example want to make sure that null values are suppressed since it's not something you can render or represent in a good way, you can set `qNullSuppression: true` when the dimension is added:
```js
dimensions: {
added(dimension) {
dimension.qNullSuppression = true;
}
}
```

232
docs/sn-create.md Normal file
View File

@@ -0,0 +1,232 @@
---
id: sn-create
title: Quick start
---
This guide will walk you through creating a simple supernova project that renders a table.
It will include the following steps:
1. Create a project
1. Configure data structure
1. Render data
1. Select data
and requires that you have:
- Access to a Qlik Sense installation.
- `node.js` `v10.0.0+` installed on your machine.
- A decent IDE, we recommend [VSCode](https://code.visualstudio.com/).
## Create a project
The quickest way to get started is to use the `nebula.js` CLI:
```bash
$ npx @nebula.js/cli create hello --picasso none
```
The command will scaffold a project into the `hello` folder with the following structure:
- `/src`
- `index.js` - Main entry point of this supernova
- `object-properties.js` - Object properties stored in the app
- `data.js` - Data configuration
- `/test` - Integration tests
- `package.json`
The folder contains some additional dotfiles files that provides linting and formatting of code.
### Start the development server
Start the development server with:
```bash
$ cd hello
$ npm run start
```
The command will start a local development server and open up http://localhost:8080 in your browser.
![Connect to engine](./assets/hub-connect.png)
The development server needs to connect to a Qlik Associative Engine running in any of the Qlik's deployments. Enter the WebSocket URL that corresponds to the Qlik product you are using.
Next, select an app to connect to.
![Connect to app](./assets/hub-app.png)
You will then be redirected to the main developer UI where you should see your supernova rendered:
![Dev](./assets/hub-dev.png)
Any updates in `/src/index.js` that affects the output will automatically cause a refresh of the supernova and you will see the changes immediately.
## Configure data structure
A simple `Hello` message is not really that useful, time to add some data.
Add a `qHyperCubeDef` definition in `object-properties.js`:
```js
const properties = {
qHyperCubeDef: {
qInitialDataFetch: [{ qWidth: 2, qHeight: 10 }],
},
// ...
};
```
Then add that hypercube as a data target in `data.js`:
```js
export default {
targets: [
{
path: '/qHyperCubeDef',
},
],
};
```
With only those changes you should now have the option to add data from the property panel on the right:
![Data targets](./assets/tutorial-data-targets.png)
Add a dimension by clicking on **Add dimension** and selecting a value in the menu that appears, do the same with measure.
## Render data
In order to render the data you first need to access it through the `useLayout` hook:
```js
import { useElement, useLayout } from `@nebula.js/nucleus`;
// ...
component() {
console.log(useLayout());
}
```
You can then `useLayout` in combination with `useEffect` to render the headers and rows of data in `qHyperCube`:
```js
component() {
const element = useElement();
const layout = useLayout();
useEffect(() => {
if (layout.qSelectionInfo.qInSelections) {
// skip rendering when in selection mode
return;
}
const hc = layout.qHyperCube;
// headers
const columns = [...hc.qDimensionInfo, ...hc.qMeasureInfo].map((f) => f.qFallbackTitle);
const header = `<thead><tr>${columns.map((c) => `<th>${c}</th>`).join('')}</tr></thead>`;
// rows
const rows = hc.qDataPages[0].qMatrix
.map((row) => `<tr>${row.map((cell) => `<td>${cell.qText}</td>`).join('')}</tr>`)
.join('');
// table
const table = `<table>${header}<tbody>${rows}</tbody></table>`;
// output
element.innerHTML = table;
}, [element, layout])
}
```
![Data table](./assets/tutorial-table.png)
## Select data
Before selecting data, we need to add some meta data on each row so that we know which one to select:
```js
// rows
const rows = hc.qDataPages[0].qMatrix
.map((row, rowIdx) => `<tr data-row="${rowIdx}">${row.map((cell) => `<td>${cell.qText}</td>`).join('')}</tr>`)
.join('');
```
And then add a `'click'` event handler on `element` which does the following:
- Verifies that the clicked element is a `td`
- Begins selections in `/qHyperCubeDef` if not already activated
- Extracts the `data-row` index from `tr`
- Updates `selectedRows` based on the click `data-row`
```js
const element = useElement();
const selections = useSelections();
const [selectedRows, setSelectedRows] = useState([]);
useEffect(() => {
const listener = (e) => {
if (e.target.tagName === 'TD') {
if (!selections.isActive()) {
selections.begin('/qHyperCubeDef');
}
const row = +e.target.parentElement.getAttribute('data-row');
setSelectedRows((prev) => {
if (prev.includes(row)) {
return prev.filter((v) => v !== row);
}
return [...prev, row];
});
}
};
element.addEventListener('click', listener);
return () => {
element.removeEventListener('click', listener);
};
}, [element]);
```
Next, we update the styling of the selected rows in the table whenever they change:
```js
useEffect(() => {
if (!layout.qSelectionInfo.qInSelections) {
// no need to update when not in selection mode
return;
}
element.querySelectorAll('tbody tr').forEach((tr) => {
const idx = +tr.getAttribute('data-row');
tr.style.backgroundColor = selectedRows.includes(idx) ? '#eee' : '';
});
}, [element, selectedRows]);
```
Finally, apply the selected values through the `selections` API:
```js
useEffect(() => {
if (selections.isActive()) {
if (selectedRows.length) {
selections.select({
method: 'selectHyperCubeCells',
params: ['/qHyperCubeDef', selectedRows, []],
});
} else {
selections.select({
method: 'resetMadeSelections',
params: [],
});
}
} else if (selectedRows.length) {
setSelectedRows([]);
}
}, [selections.isActive(), selectedRows]);
```
## Next steps
- [Supernova introduction](./sn-component.md)

29
docs/sn-introduction.md Normal file
View File

@@ -0,0 +1,29 @@
---
id: sn-introduction
title: Introduction
---
## What is a supernova?
A supernova in the context of this API represents the visual output of some underlying data stored in Qlik's Associative Data Model. It could be almost anything you want it to be and is traditionally developed to show the data in the shape of a chart, table, kpi etc.
## Composition
A supernova has two main parts: a backend _Generic Object_ that describes the _properties_ of the supernova and is persisted in the data model, and a frontend visual part that renders the _layout_ of the _Generic Object_.
## Definition
The minimal supernova that doesn't contain any data nor renders anything looks like this:
```js
export default function () {
return {
qae: {
/* */
},
component() {},
};
}
```
The `component()` is where you render the visual part, while `qae` is where you define the properties and data handled by Qlik's Associative Engine (QAE).

View File

@@ -1,6 +1,7 @@
# @nebula.js/supernova
> version: 0.2.0
---
id: sn-reference
title: API Reference
---
## Table of contents
@@ -219,7 +220,7 @@ import { useModel } from '@nebula.js/supernova';
// ...
const model = useModel();
useEffect(() => {
model.getInfo().then(info => {
model.getInfo().then((info) => {
console.log(info);
});
}, []);
@@ -236,7 +237,7 @@ import { useApp } from '@nebula.js/supernova';
// ...
const app = useApp();
useEffect(() => {
app.getAllInfos().then(infos => {
app.getAllInfos().then((infos) => {
console.log(infos);
});
}, []);
@@ -254,7 +255,7 @@ import { useGlobal } from '@nebula.js/supernova';
// ...
const g = useGlobal();
useEffect(() => {
g.engineVersion().then(version => {
g.engineVersion().then((version) => {
console.log(version);
});
}, []);
@@ -327,7 +328,7 @@ const act = useAction(
hidden: false,
disabled: zoomed,
action() {
setZoomed(prev => !prev);
setZoomed((prev) => !prev);
},
icon: {},
}),
@@ -422,7 +423,7 @@ import { useLayout } from '@nebula.js/supernova';
const layout = useLayout();
const [zoomed] = useState(layout.isZoomed || false);
onTakeSnapshot(copyOfLayout => {
onTakeSnapshot((copyOfLayout) => {
copyOfLayout.isZoomed = zoomed;
return Promise.resolve(copyOfLayout);
});
@@ -438,7 +439,7 @@ The entry point for defining a supernova.
```js
import { useElement, useLayout } from '@nebula.js/supernova';
export default function() {
export default function () {
return {
qae: {
properties: {

137
docs/sn-using-data.md Normal file
View File

@@ -0,0 +1,137 @@
---
id: sn-using-data
title: Using data
---
## Access layout
You can access the layout of the Generic Object through a set of predefined hooks.
### useLayout
`useLayout` returns the evalutated layout of the Generic Object's properties:
```js
import { useLayout } from '@nebula.js/supernova';
export default function () {
return {
qae: {
properties: {
qHyperCubeDef: {},
simpleMath: {
qValueExpression: {
qExpr: '1+1',
},
},
},
},
component() {
const layout = useLayout();
console.log(layout); // { qHyperCube: {/* HyperCube Layout */}, simpleMath: 2 }
},
};
}
```
You should `useEffect` when observing changes on the `layout`:
```js
const layout = useLayout();
useEffect(() => {
// do some heavy update
}, [layout]);
```
### useAppLayout
`useAppLayout` returns the [NxAppLayout](https://core.qlik.com/services/qix-engine/apis/qix/definitions/#nxapplayout) you are currently connected to:
```js
import { useAppLayout } from '@nebula.js/supernova';
export default function () {
return {
component() {
const appLayout = useAppLayout();
console.log(appLayout); // { qTitle: 'App title', qLocaleInfo: {/* */ } }
},
};
}
```
The most common use case for the app layout is to access `qLocaleInfo` which contains locale details selected by the app owner and should be used to format numbers.
## Models
In addition to the layouts of the app and generic object, you have full access to the APIs generated by `enigma.js`. These APIs are generated from a JSON-RPC schema and unlocks the full power of Qlik's Associate Engine.
### useModel
`useModel` returns the generated API of the [Generic Object](https://core.qlik.com/services/qix-engine/apis/qix/genericobject/) of your supernova:
```js
import { useModel } from '@nebula.js/supernova';
export default function () {
return {
component() {
const model = useModel();
model.getInfo().then((info) => {
console.log(info);
});
},
};
}
```
Common operations in this API is to:
- make selections with `beginSelections`, `selectHyperCubeValues` and `endSelections`
- get more data with `getHyperCubeData`
### useApp
`useApp` returns the generated API of the [Doc](https://core.qlik.com/services/qix-engine/apis/qix/doc/) your supernova belongs to:
```js
import { useApp } from '@nebula.js/supernova';
export default function () {
return {
component() {
const app = useApp();
app.clearAll();
},
};
}
```
Common operations in this API is to:
- modify the selection stack with `clearAll`, `back`, `forward`
- create and apply bookmarks with `createBookmark` and `applyBookmark`
### useGlobal
`useGlobal` returns the generated API of the [Global](https://core.qlik.com/services/qix-engine/apis/qix/global/) your supernova belongs to:
```js
import { useGlobal } from '@nebula.js/supernova';
export default function () {
return {
component() {
const g = useGlobal();
g.engineVersion().then((v) => {
console.log(v);
});
},
};
}
```
Common operations in this API is to:
- get a list of apps with `getDocList`

View File

@@ -23,11 +23,29 @@
"render-charts": {
"title": "Rendering supernovae"
},
"sn-component": {
"title": "Rendering"
},
"sn-configure-data": {
"title": "Configuring data"
},
"sn-controller": {
"title": "Modify charts"
},
"supernova-api": {
"title": "supernova-api"
"sn-create": {
"title": "Quick start"
},
"sn-introduction": {
"title": "Introduction"
},
"sn-reference": {
"title": "API Reference"
},
"sn-selections": {
"title": "Selecting data"
},
"sn-using-data": {
"title": "Using data"
},
"web-integration": {
"title": "Web integration"
@@ -36,7 +54,8 @@
"links": {},
"categories": {
"Getting started": "Getting started",
"Nucleus guide": "Nucleus guide"
"Nucleus guide": "Nucleus guide",
"Supernova guide": "Supernova guide"
}
},
"pages-strings": {

View File

@@ -1,6 +1,7 @@
{
"docs": {
"Getting started": ["introduction", "installation", "web-integration"],
"Nucleus guide": ["nucleus-configuration", "render-charts", "app-selections", "nucleus-reference"]
"Getting started": ["introduction", "installation", "web-integration", "sn-create"],
"Nucleus guide": ["nucleus-configuration", "render-charts", "app-selections", "nucleus-reference"],
"Supernova guide": ["sn-introduction", "sn-component", "sn-configure-data", "sn-using-data", "sn-reference"]
}
}