User guide

Creating a new application

Lens is framework agnostic, so there are many ways to build and deploy your application. This guide focuses on compatibility with other projects in the Samply organization, so we use SvelteKit as the frontend framework and Docker for deployment.

To create a new SveltKit application run npx sv create my-app. Use the minimal template with TypeScript syntax and select Prettier and ESLint when prompted.

Now cd to the new directory and install Lens:

npm install @samply/lens

Prettier config

The Prettier config created by sv create uses tabs and sets the print width to 100 against the recommendation of Prettier. We recommend to remove these options from .prettierrc and use the Prettier defaults with the Svelte plugin only:

{
    "plugins": ["prettier-plugin-svelte"],
    "overrides": [
        {
            "files": "*.svelte",
            "options": {
                "parser": "svelte"
            }
        }
    ]
}

Configuring the root route

Typically your application will only use the root route at src/routes. We will import the Lens CSS and JS bundles and render the main application component. Because Lens uses Web Components we need to disable HMR and SSR. Change the content of src/routes/+page.svelte to:

<script>
    // Using hot module replacement (HMR) with custom elements (aka web
    // components) does not work because a custom element cannot be updated once
    // registered, see https://github.com/WICG/webcomponents/issues/820.
    // Therefore we do a full page reload instead of HMR.
    if (import.meta.hot) {
        import.meta.hot.on('vite:beforeUpdate', () => {
            window.location.reload();
        });
    }

    // Import Lens CSS and JS bundles
    import "@samply/lens/style.css";
    import "@samply/lens";

    import App from '../App.svelte';
</script>

<App />

And add src/routes/+page.ts:

// Using server-side rendering (SSR) with custom elements (aka web components)
// causes issues because the server does not know that an element is a Svelte
// component and converts all props to strings.
export const ssr = false;

The application component

Your main application code lives in the application component. Create the file src/app.css and leave it empty and create src/App.svelte with the following content:

<script lang="ts">
    import "./app.css";
</script>

<lens-search-button></lens-search-button>

Now run npm run dev and open http://localhost:5173/ in your browser. You should see a search button in the top left corner of the page.

Options and catalogue

Your application must pass two objects to Lens. The LensOptions object contains general configuration options and the Catalogue object describes what users can search for. You can define these objects in TypeScript but many applications in the Samply organization define them in JSON files.

Assuming you are using JSON files, create the file src/options.json containing the empty object {} and the file src/catalogue.json with the following content:

[
    {
        "key": "rh_factor",
        "name": "Rh factor",
        "system": "",
        "fieldType": "single-select",
        "type": "EQUALS",
        "criteria": [
            {
                "key": "rh_positive",
                "name": "Rh+"
            },
            {
                "key": "rh_negative",
                "name": "Rh-"
            }
        ]
    }
]

Add the following to the top of src/App.svelte to load the JSON files and pass the objects to Lens:

<script lang="ts">
    import { onMount } from "svelte";
    import {
        setOptions,
        setCatalogue,
        type LensOptions,
        type Catalogue,
    } from "@samply/lens";
    import options from "./options.json";
    import catalogue from "./catalogue.json";
    onMount(() => {
        setOptions(options as LensOptions);
        setCatalogue(catalogue as Catalogue);
    });
</script>

<lens-catalogue></lens-catalogue>

When you run npm run dev you should see the catalogue component with the "Rh factor" item.

Schema validation

Lens includes JSON schema definitions for the options and the catalogue type. Create the script scripts/validate-json-schema.bash to validate your JSON files against the schema definitions:

set -e # Return non-zero exit status if one of the validations fails
npx ajv validate -c ajv-formats -s node_modules/@samply/lens/schema/options.schema.json -d src/options.json
npx ajv validate -c ajv-formats -s node_modules/@samply/lens/schema/catalogue.schema.json -d src/catalogue.json

Then install the required dependencies and test the script:

npm install ajv-cli ajv-formats --save-dev
bash scripts/validate-json-schema.bash

You can also configure VS Code to validate your JSON files against the schema definitions. This will show validation errors in your editor and provide IntelliSense. To do so add the following configuration to your workspace settings in VS Code:

"json.schemas": [
    {
        "fileMatch": [
            "catalogue*.json"
        ],
        "url": "./node_modules/@samply/lens/schema/catalogue.schema.json",
    },
        {
        "fileMatch": [
            "options*.json"
        ],
        "url": "./node_modules/@samply/lens/schema/options.schema.json",
    },
]

Test environment

It is a common requirement to load different options in test and production. You can achieve this by using a feature of SvelteKit that makes environment variables from the server available in the browser. Applications in the Samply organization commonly accept the following environment variables:

  • PUBLIC_ENVIRONMENT: Accepts the name of the environment, e.g. production or test
  • PUBLIC_BACKEND_URL: Overwrites the URL of the backend that your application queries

For example you could handle the PUBLIC_ENVIRONMENT variable as follows:

<script lang="ts">
       import { env } from "$env/dynamic/public";
       ...
    onMount(() => {
           setOptions(env.PUBLIC_ENVIRONMENT === "test" ? testOptions : prodOptions);
    });
       ...
</script>

Deployment

We recommend that projects in the Samply organization follow these deployment practices. We will use Node.js inside Docker. Run npm install @sveltejs/adapter-node and change the adapter in svelte.config.js:

-import adapter from '@sveltejs/adapter-auto';
+import adapter from '@sveltejs/adapter-node';

Then you can remove @sveltejs/adapter-auto from package.json. Now create a Dockerfile with the following content:

FROM node:22-alpine AS builder
WORKDIR /app

# Install dependencies first to leverage Docker cache
COPY package.json package-lock.json ./
RUN npm ci

# Copy the rest of the application
COPY vite.config.ts svelte.config.js ./
COPY src ./src
COPY static ./static

# Build the SvelteKit project
RUN npm run build

# Production image
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/build ./build
EXPOSE 3000
CMD ["node", "build"]

To automatically build Docker images and publish them to Docker Hub when a branch changes, we recommend to use the Samply Docker CI workflow for GitHub Actions. Use the workflow template or copy the following into .github/workflows/docker.yml:

# This workflow builds a Docker image from the Dockerfile and publishes the
# image to Docker Hub. How the image tags are chosen is documented here:
# https://github.com/samply/github-workflows/blob/main/.github/workflows/docker-ci.yml
#
# This file is copied and adapted from:
# https://github.com/samply/.github/blob/main/workflow-templates/docker-ci-template.yml

name: Docker CI

on:
    push:
        branches:
            - main
            - develop
        # Build when a new version is tagged
        tags:
            - "v*.*.*"
    pull_request:
        branches:
            - main
            - develop
jobs:
    build:
        # This workflow defines how a samply docker image is built, tested and published.
        # Visit: https://github.com/samply/github-workflows/blob/main/.github/workflows/docker-ci.yml, for more information
        uses: samply/github-workflows/.github/workflows/docker-ci.yml@main
        with:
            # The Docker Hub Repository you want eventually push to, e.g samply/share-client
            image-name: "samply/your-project"
            # Where to push your images ("dockerhub", "ghcr", "both" or "none")
            push-to: dockerhub
        # This passes the secrets from calling workflow to the called workflow
        secrets:
            DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
            DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

Linting in GitHub Actions

You can use GitHub Actions to run the following checks on pull requests:

  • svelte-check to check for TypeScript compiler errors
  • Prettier
  • ESLint
  • Test that the build works
  • Validate catalogue and options

To do so create .github/workflows/linting.yml with the following content:

name: Linting
on:
    pull_request:
        branches:
            - main
            - develop
    push:
        branches:
            - develop

jobs:
    verify-code:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
            - run: npm ci
            - run: npx svelte-check
            - run: npx prettier --check .
            - run: npx eslint .
            - run: npx vite build
            - run: bash scripts/validate-json-schema.bash

AST

AST, short for abstract Syntax Tree is a standard definiton of Lens

Overwriting styles

Translations

Lens supports English and German language out of the box. You can set the language to en or de in the Lens options:

"language": "de"

Overwriting texts

You can overwrite the built-in translations in the Lens options to customize the text:

"texts": {
    "loading": {
        "en": "Processing..."
    }
}

Or add translations for new languages:

"texts": {
    "loading": {
        "es": "Cargando..."
    }
}

Or you can add your own texts that you can then translate in your application. We recommend that you prefix the key with your project name to avoid collisions:

"texts": {
    "ccp-welcome": {
        "en": "Welcome to CCP Explorer!",
        "de": "Willkommen beim CCP Explorer!"
    }
}

Using translations in your application

You can also use the translations in your application:

<script lang="ts">
    import { translate } from "@samply/lens";
</script>

<span>{translate("loading")}</span>

Development

Release notes

Version 0.5

Lens version 0.5 paves the way for better documentation of the Lens library with the introduction of the Lens Book and API docs. Version 0.5 also introduces the translations API and the setOptions and setCatalogue APIs that pose an alternative to the <lens-options></lens-options> component. Finally it comes with internal improvements such as the migration to Svelte 5 and the automatic generation of the catalogue JSON schema to keep it in sync with the TypeScript definitions. You can find the full list of changes on GitHub.

Migration guide

Version 0.5 introduces a few breaking changes that application authors should be aware of. This guide will help you to migrate your application to the new version.

CSS bundle import path

The import path of the CSS bundle has changed. If the import is processed by your bundler we recommend to use automatic module resolution:

import "@samply/lens/style.css";

If you have to refer to the file directly you can find it at node_modules/@samply/lens/dist/lens.css.

Catalogue changes

The catalogue JSON schema is a little bit stricter now. Most notably every object with the "childCategories" property now additionally requires "fieldType": "group". For example:

 {
+    "fieldType": "group",
     "key": "donor",
     "name": "Donor/Clinical Information",
     "childCategories": [
         ...
     ]
 }

You can use the following this script to update your catalogue. Afterwards open the file in your editor and reformat it.

sed -i 's/"childCategories"/"fieldType": "group", "childCategories"/' catalogue.json

Catalogue icons

Icons have been removed from the <lens-catalogue> component:

 <lens-catalogue
-    toggleIconUrl="right-arrow-svgrepo-com.svg"
-    addIconUrl="long-right-arrow-svgrepo-com.svg"
-    infoIconUrl="info-circle-svgrepo-com.svg"
     ...
 ></lens-catalogue>

Add them to the iconOptions object in the Lens options instead. Also remove the selectAll property and use the new translations API instead if you want to customize the text.

 "iconOptions": {
     "deleteUrl": "delete_icon.svg",
     "infoUrl": "info-circle-svgrepo-com.svg",
+    "toggleIconUrl": "right-arrow-svgrepo-com.svg",
+    "addIconUrl": "long-right-arrow-svgrepo-com.svg",
-    "selectAll": {
-        "text": "Alle Hinzufügen"
-    }
 }

Schema validation

JSON schema definitions for the Lens options and the catalogue are now included in the Lens release. Refer to this section in the new application guide to learn how to validate your JSON files against the JSON schema.

Branching

Gitflow:

  1. The branch develop is created from main
  2. The branch release is created from develop
  3. Feature branches are created from develop
  4. When a feature is complete it is merged into the develop branch
  5. When the release branch is done it is merged into develop and main
  6. If an urgent issue in main is detected a hotfix branch is created from main
  7. Once the hotfix is complete it is merged to both develop and main

For reference

Versioning

For MAIN/ RELEASE branches

Given a version number MAJOR.MINOR.PATCH, increment the:

  • MAJOR version when you make incompatible (BREAKING) API changes
  • MINOR version when you add functionality in a backward compatible manner
  • PATCH version when you make backward compatible bug fixes

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

Please see Semantic Versioning

Commits:

<TYPE>[optional scope]: <description>

[optional body]

[optional footer(s)]
  1. FIX: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
  2. FEAT: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
  3. Choose one -> BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
  4. Types other than fix: and feat: ci:, docs:, style:, refactor:.

SemVer -> fix type commits should be translated to PATCH releases. feat type commits should be translated to MINOR releases. Commits with BREAKING CHANGE in the commits, regardless of type, should be translated to MAJOR releases.

See more: Conventional Commits

Liniting

Install

npm install --dev

Run

npm run lint

Note: this will only lint staged files, so don't forget to add with git add

Setup

Project

First setup your project with a framework of your choice (Svelte, React, Vue, Angular,...).

Then run

npm install @samply/lens


Configuration

Use the Lens Options Component to fill in your configuration

<lens-options options={yourlibraryOptions} catalogueData={yourCatalogueData} />
  • options takes the general configuration for the library as JSON.
  • catalogueData takes a catalogue of search criteria, also as JSON.

Schemas

options
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Lens Options",
    "description": "The options for the lens",
    "type": "object",
    "properties": {
        "iconOptions": {
            "type": "object",
            "properties": {
                "infoUrl": {
                    "type": "string",
                    "pattern": "^.+$",
                    "description": "The icon to use for the info button"
                },
                "addUrl": {
                    "type": "string",
                    "pattern": "^.+$",
                    "description": "The icon to use for the add button in the catalogue"
                },
                "toggleUrl": {
                    "type": "string",
                    "pattern": "^.+$",
                    "description": "The icon to use for the toggle button in the catalogue"
                }
            },
            "additionalProperties": false,
            "unevaluatedProperties": false,
            "required": []
        },
        "chartOptions": {
            "type": "object",
            "patternProperties": {
                "^.+$": {
                    "type": "object",
                    "properties": {
                        "legendMapping": {
                            "type": "object",
                            "patternProperties": {
                                "^.+$": {
                                    "type": "string",
                                    "pattern": "^.+$"
                                }
                            },
                            "additionalProperties": false,
                            "unevaluatedProperties": false,
                            "required": []
                        },
                        "hintText": {
                            "type": "array",
                            "items": {
                                "type": "string",
                                "pattern": "^.+$",
                                "description": "The hint text to display as overlay of the info button"
                            }
                        },
                        "aggregations": {
                            "type": "array",
                            "description": "add strings of other data keys to include in the chart",
                            "items": {
                                "type": "string",
                                "pattern": "^.+$"
                            }
                        },
                        "tooltips": {
                            "type": "object",
                            "patternProperties": {
                                "^.+$": {
                                    "type": "string",
                                    "pattern": "^.+$",
                                    "description": "The tooltip to display while hovering over the chart data"
                                }
                            },
                            "additionalProperties": false,
                            "unevaluatedProperties": false,
                            "required": []
                        }
                    },
                    "additionalProperties": false,
                    "unevaluatedProperties": false,
                    "required": []
                }
            },
            "additionalProperties": false,
            "unevaluatedProperties": false,
            "required": []
        },
        "tableOptions": {
            "type": "object",
            "properties": {
                "headerData": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": {
                                "type": "string",
                                "description": "the title of the column",
                                "pattern": "^.+$"
                            },
                            "dataKey": {
                                "type": "string",
                                "description": "a single key to display in the table",
                                "pattern": "^.+$"
                            },
                            "aggregatedDataKeys": {
                                "type": "array",
                                "description": "an array of keys to aggregate and display in the table as single value",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "groupCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        },
                                        "stratifierCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        },
                                        "stratumCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        }
                                    },
                                    "additionalProperties": false,
                                    "unevaluatedProperties": false,
                                    "required": []
                                }
                            }
                        },
                        "additionalProperties": false,
                        "unevaluatedProperties": false,
                        "required": [
                            "title"
                        ]
                    }
                }
            },
            "additionalProperties": false,
            "unevaluatedProperties": false,
            "required": ["headerData"]
        },
        "resultSummaryOptions": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "pattern": "^.+$"
                },
                "infoButtonText": {
                    "type": "string",
                    "pattern": "^.+$"
                },
                "dataTypes": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": {
                                "type": "string",
                                "pattern": "^.+$"
                            },
                            "dataKey": {
                                "type": "string",
                                "pattern": "^.+$"
                            },
                            "aggregatedDataKeys": {
                                "type": "array",
                                "description": "an array of keys to aggregate and display in the result summary as single value",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "groupCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        },
                                        "stratifierCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        },
                                        "stratumCode": {
                                            "type": "string",
                                            "pattern": "^.+$"
                                        }
                                    },
                                    "additionalProperties": false,
                                    "unevaluatedProperties": false,
                                    "required": []
                                }
                            }
                        },
                        "additionalProperties": false,
                        "unevaluatedProperties": false,
                        "required": []
                    }
                }
            },
            "additionalProperties": false,
            "unevaluatedProperties": false,
            "required": []
        }
    },
    "additionalProperties": false,
    "unevaluatedProperties": false,
    "required": []
}
catalogueData
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "title": "Search Parameter Catalogue",
    "description": "A catalogue of search parameters",
    "type": "array",
    "items": {
        "$ref": "#/$defs/categoryItem"
    },
    "$defs": {
        "childCategories": {
            "type": "array",
            "items": {
                "$ref": "#/$defs/categoryItem"
            }
        },
        "categoryItem": {
            "type": "object",
            "properties": {
                "key": {
                    "type": "string",
                    "pattern": "^.+$"
                },
                "name": {
                    "type": "string",
                    "pattern": "^.+$"
                },
                "subCategoryName": {
                    "type": "string",
                    "pattern": "^.+$"
                },
                "infoButtonText": {
                    "type": "array",
                    "description": "The text to display in the info button",
                    "items": {
                        "type": "string",
                        "pattern": "^.*$"
                    }
                },
                "system": {
                    "type": "string",
                    "pattern": "^.*$"
                },
                "fieldType": {
                    "enum": [
                        "single-select",
                        "number",
                        "autocomplete"
                    ]
                },
                "type": {
                    "enum": [
                        "EQUALS",
                        "BETWEEN"
                    ]
                },
                "childCategories": {
                    "$ref": "#/$defs/childCategories"
                },
                "criteria": {
                    "$ref": "#/$defs/criteria"
                }
            },
            "additionalProperties": false,
            "unevaluatedProperties": false,
            "required": [
                "key",
                "name"
            ]
        },
        "criteria": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "key": {
                        "type": "string",
                        "pattern": "^.+$"
                    },
                    "name": {
                        "type": "string",
                        "pattern": "^.+$"
                    },
                    "description": {
                        "type": "string",
                        "pattern": "^.*$"
                    },
                    "infoButtonText": {
                        "type": "array",
                        "description": "The text to display in the info button",
                        "items": {
                            "type": "string",
                            "pattern": "^.+$"
                        }
                    },
                    "aggregatedValue": {
                        "type": "array",
                        "items": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "value": {
                                        "type": "string",
                                        "pattern": "^.+$"
                                    },
                                    "name": {
                                        "type": "string",
                                        "pattern": "^.+$"
                                    }
                                },
                                "additionalProperties": false,
                                "unevaluatedProperties": false,
                                "required": [
                                    "value",
                                    "name"
                                ]
                            }
                        }
                    }
                },
                "additionalProperties": false,
                "unevaluatedProperties": false,
                "required": [
                    "key",
                    "name"
                ]
            }
        }
    }
}


How to use Lens components

Here we use a minimal setup with a search tree, an autocomplete search bar, the search button and a simple chart

Place the following components in your application where they are needed.

Catalogue

<lens-catalogue />

displays a catalogue navigation

<lens-search-bar noMatchesFoundMessage={"No matches found"} />

Search Button

<lens-search-button title="Search" />

Bar Chart

<lens-chart
    title="Alter bei Erstdiagnose"
    catalogueGroupCode="age_at_diagnosis"
    chartType="bar"
/>
  • title: the title to show as heading in the chart
  • atalogueGroupCode: the key of your childCategory in the catalogue
  • chartType: the type of the chart (currently supports: bar for bar charts and pie for pie charts


Styling the components

The library provides a default styling.

You can import it in your main css file like this:

@import "<path-to>/node_modules/@samply/lens/dist/style.css";

However you can override these styles using css (or your favorite preprocessor) with the web component syntax.

lens-catalogue::part(number-input-formfield) {
  width: 60px;
  margin-left: 20px;
  border: solid 1px dark-gray;
  border-radius: 0;
  text-align: center;
  font-size: 14px;
}

The styling with parts is scoped to the lens-component and does not affect other components. You can also use pseudo classes like this:

lens-catalogue::part(number-input-formfield):focus {
  border-color: blue;
  outline: none;
}

Make sure to add your custom styles after the import.

Lens Component Styling

Search Modified Button

The search-modified button does not include any default styling. To customize its appearance, you need to apply your own CSS styles using the provided CSS class:

lens-search-modified-display::part(display-wrapper){ }