{"id":17222362,"url":"https://github.com/yoerinijs/vienna","last_synced_at":"2026-02-07T10:30:56.541Z","repository":{"id":41935127,"uuid":"410625147","full_name":"YoeriNijs/vienna","owner":"YoeriNijs","description":"A micro frontend for creating webcomponents with ease","archived":false,"fork":false,"pushed_at":"2024-12-11T14:20:19.000Z","size":721,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-12T10:41:32.746Z","etag":null,"topics":["framework","javascript","typescript","webcomponents"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/YoeriNijs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-09-26T18:02:11.000Z","updated_at":"2024-12-11T14:20:22.000Z","dependencies_parsed_at":"2022-08-28T08:10:11.522Z","dependency_job_id":null,"html_url":"https://github.com/YoeriNijs/vienna","commit_stats":null,"previous_names":[],"tags_count":51,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YoeriNijs%2Fvienna","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YoeriNijs%2Fvienna/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YoeriNijs%2Fvienna/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YoeriNijs%2Fvienna/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/YoeriNijs","download_url":"https://codeload.github.com/YoeriNijs/vienna/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248799903,"owners_count":21163400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","javascript","typescript","webcomponents"],"created_at":"2024-10-15T04:05:00.429Z","updated_at":"2026-02-07T10:30:56.533Z","avatar_url":"https://github.com/YoeriNijs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Vienna micro frontend\n\n![Vienna logo](https://raw.githubusercontent.com/YoeriNijs/vienna/main/img/logo.png)\n\n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)\n\n## Do great things with this tiny framework\n\nVienna is a micro framework written in TypeScript and under active development. It is based on some core concepts of\nthe [Angular](https://angular.io) and [FatFree](https://fatfreeframework.com/) frameworks. Since Vienna is still work in\nprogress, it might be unstable.\n\nCheck out the [demo application](https://github.com/YoeriNijs/vienna-demo-app).\n\n## Table of Contents (work in progress)\n\n- [Install Vienna](#install)\n- [Set up application](#set-up-application)\n- [Component creation](#create-components)\n- [Component binding](#component-binding)\n    - [Input binding](#input-binding)\n    - [Output binding](#output-binding)\n    - [Element binding](#element-binding)\n- [Component lifecycle hooks](#component-lifecycle-hooks)\n- [Conditional segments](#conditional-segments)\n    - [VCheck](#vcheck)\n    - [VRepeat](#vrepeat)\n    - [VSwitch](#vswitch)\n- [Event binding](#event-binding)\n- [Pipes](#pipes)\n    - [Raw](#raw)\n    - [Json](#json)\n    - [Encode base64](#encode-base64)\n    - [Decode base64](#decode-base64)\n    - [Custom pipes](#create-custom-pipes)\n- [Routes](#routes)\n    - [Nested routes](#nested-routes)\n    - [Route wildcards](#route-wildcards)\n    - [Route data](#route-data)\n    - [Route params](#route-params)\n    - [Query params](#query-params)\n    - [Guards](#route-guards)\n    - [Route redirects](#route-redirects)\n    - [SEO optimization](#seo-optimization)\n- [Dependency injection](#dependency-injection)\n- [Dark mode](#dark-mode)\n    - [Set up dark mode](#set-up-dark-mode)\n    - [Customize dark mode](#customize-dark-mode)\n- [Validator](#validator)\n- [I18n](#i18n)\n    - [Global i18n configuration](#global-i18n-configuration)\n    - [Dynamic i18n configuration](#dynamic-i18n-configuration)\n- [Miscellaneous](#miscellaneous)\n    - [VAudit](#vaudit)\n    - [VWeb](#vweb)\n        - [Slugify](#slugify)\n        - [Cookies](#cookies)\n        - [Document tags](#override-document-tags)\n- [Plugins](#plugins)\n    - [Logger](#logger)\n- [Component testing](#component-testing)\n\n## Install\n\n```\nnpm install vienna-ts\n```\n\nOr start with the [starter template](https://github.com/YoeriNijs/vienna-starter-template) right away.\n\n## Set up application\n\nIn order to get started with Vienna, you need a root class that holds the `VApplication` decorator. This root class is\nresponsible for all declarations, routes and other application wide configuration. At the moment, declarations and\nroutes are mandatory, so:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [],\n    routes: []\n})\nexport class Application {}\n```\n\nTo get started, just create a [component](#create-components) and add the component to your declarations. Furthermore,\nspecify the root route by adding a [route](#routes) that holds the root path:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [HomeComponent],\n    routes: [\n      { path: '/', component: HomeComponent }\n    ]\n})\nexport class Application {}\n```\n\n\u003cb\u003eDon't forget to actually declare the application class. Otherwise, your app won't run!\u003c/b\u003e\n\nCurrently, the `VApplication` decorator accepts a so-called `VApplicationConfig`. In this config, you can specify the\nfollowing:\n\n- `declarations` (mandatory): holds all Vienna components that you want to use in your application.\n- `routes` (mandatory): holds all Vienna routes for your application.\n- `routeNotFoundStrategy` (optional): can be used to specify another strategy if a route is not found. Currently, Vienna\n  accepts the following strategies:\n    - `Ignore`: enum. If configured, the invalid route will be ignored and the user stays on the current page.\n    - `Root`: enum. If configured, the user will be navigated back to the root route the Vienna application.\n    - `VRouteNotFoundRedirect`: object that holds a path field. The router will send the user to the configured path.\n      The path should start with '/'. Example object: `{ path: '/not-found' }`.\n    - `VCustomRouteRedirect`: object that needs to have a method called `redirectTo` that does not take any arguments, and just returns a route string, such as `/my-path`. Of course, you need to have an implementation for this route.\n- `rootElementSelector` (optional): can be used to specify which root element should be used by Vienna as application\n  root. Default: 'body'.\n- `globalStyles` (optional): can be used to inject global styles in every webcomponent. This might be handy if you want\n  to use a css (utility) framework, such as [Tailwind](https://tailwindcss.com) or [Bulma](https://bulma.io).\n  GlobalStyles support two kinds of globals:\n    - `style`: just plain css.\n    - `href`: a link to a remote stylesheet. Thus, a href should start with 'http'. Also, it is possible to pass\n      integrity and crossorigin values  (see below).\n- `darkMode` (optional): global config object to initialize app-wide dark mode (see [dark mode](#dark-mode)).\n- `pipes` (optional): custom pipes (see [create custom pipes](#create-custom-pipes))\n\nFor instance, if you want to use [Bulma](https://bulma.io), just add the following:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [HomeComponent],\n    routes: [\n      { path: '/', component: HomeComponent }\n    ],\n    globalStyles: [\n      {\n        href: 'https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css',\n        integrity: \"sha384-IJLmUY0f1ePPX6uSCJ9Bxik64/meJmjSYD7dHaJqTXXEBE4y+Oe9P2KBZa/z7p0Q\",\n        crossOrigin: \"anonymous\"\n      }\n      { style: 'body { padding: 10px; }' }\n    ]\n})\nexport class Application {}\n```\n\n## Create components\n\nVienna is based on the concept of components. A component is a small piece of code that holds it's own encapsulated\nlogic and styling. One is able to create various components, and a component is able to make use of other components.\n\nTo create a component in Vienna, you need the `VComponent` decorator. A component holds three properties: a selector\n(that needs to consist of at least two parts and is hyphen-separated), some styles, and html. For instance:\n\n`my-component.ts`\n\n```\n@VComponent({\n    selector: 'my-component',\n    styles: [`\n      span {\n        background-color: red;\n        color: white;\n      }\n    `],\n    html: '\u003cspan\u003eHello there!\u003c/span\u003e'\n})\nexport class MyComponent {}\n```\n\nThen, just declare the new component in your application root by appending it to the declarations array:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [MyComponent],\n    routes: []\n})\nexport class Application {}\n```\n\nCurrently, the styles array only accepts strings. The same holds for the html part: it only accepts a string for now.\n\n## Component binding\n\nWhen an application grows, one needs to pass data from one component to another. For this, it is possible to use\ncomponent binding.\n\n### Input binding\n\nSince Vienna uses the fundamentals of webcomponents, it is possible to render components inside components. In order to\npass data from one component to another, the so-called `VProp` decorator is needed. For example:\n\n`component-a.ts`\n\n```\n@VComponent({\n    selector: 'component-a',\n    styles: [],\n    html: '\u003cspan\u003e{{ message }}\u003c/span\u003e'\n})\nexport class ComponentA {\n  @VProp() message: string = '';\n}\n```\n\n`component-b.ts`\n\n```\n@VComponent({\n    selector: 'component-b',\n    styles: [],\n    html: '\u003ccomponent-a message=\"Hello from ComponentB!\"\u003e\u003c/component-a\u003e'\n})\nexport class ComponentB {}\n```\n\n### Output binding\n\nTo bind to component outputs, it is possible to implement the `VEmitter`.\n\n\u003cb\u003eImportant!\u003c/b\u003e Naming is very important when using output binding. As opposed to frameworks like Angular, Vienna does\nnot\nlimit the scope of the output to one parent. As a consequence, emitted items are available in the whole codebase.\nThis is as designed, since it may be very useful to emit the data to the complete application at once. If you do not\nwant this,\nyou are fully in control. Just keep your naming clean and unique.\n\n`component-a.ts`\n\n```\n@VComponent({\n    selector: 'component-a',\n    styles: [],\n    html: '\u003cspan\u003e{{ message }}\u003c/span\u003e'\n})\nexport class ComponentA {\n    message = 'Hello from Earth';\n    \n    @VEmit('messageChange')\n    messageChange: VEmitter\u003cstring\u003e = new VEmitter\u003cstring\u003e();\n    \n    constructor() {\n      setTimeout(() =\u003e {\n        this.message = 'Hello from Mars';\n        this.messageChange.emit(this.message);\n      }, 3000);\n    }\n}\n```\n\n`component-b.ts`\n\n```\n@VComponent({\n    selector: 'component-b',\n    styles: [],\n    html: '\u003ccomponent-a @emit=\"messageChange =\u003e printNewMessage(message)\"\u003e\u003c/component-a\u003e'\n})\nexport class ComponentB {\n  printNewMessage(message: string): void {\n    alert(`Retrieved from ComponentA: ${message}`);\n  }\n}\n```\n\n### Element binding\n\nInterested in the actual value of an element inside your view? Just add bind!\n\n`custom.component.ts`\n\n```\n@VComponent({\n    selector: 'custom-component',\n    styles: [],\n    html: '\u003cinput @bind=\"textInput\" id=\"textInput\" type=\"text\" placeholder=\"Some text\" minlength=\"1\" /\u003e'\n})\nexport class CustomComponent implement VInit {\n    textInput: HTMLInputElement;\n    \n    vInit(): void {\n        setInterval(() =\u003e console.log(this.textInput.value), 1000));\n    }\n}\n```\n\n## Component lifecycle hooks\n\nIt is possible to use a component lifecycle hook to perform some logic. Vienna supports two hooks at the moment:\n\n- `VInit`: Will be executed when the component is going to be rendered in the view.\n- `VAfterInit`: Will be executed when the root node and all child nodes are initialized and rendered in the view.\n- `VDestroy`: Will be executed when the component is removed from the view.\n\nIf you want to use these hooks, just implement the corresponding interface (i.e. `VInit`, `VAfterInit` or `VDestroy`).\nFor example:\n\n`my-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'my-component', \n  styles: [], \n  html: `Hello world!`\n})\nexport class MyComponent implements VInit, VAfterInit, VDestroy { \n  vInit(): void { \n    alert('I am executed when this component is going to be rendered!'); \n  }\n  \n  vAfterInit(): void {\n    alert('I am executed when this component is rendered!');\n  }\n\n  vDestroy(): void { \n    alert('I am executed when this component is removed!'); \n  } \n}\n\n```\n\n## Conditional segments\n\nConditional segments can help you to enrich your html by adding some view logic.\n\n### VCheck\n\nWith VCheck, you can specify when an element should be rendered by checking a value or function. It is mandatory to add\nat least one true or one false element, but you do not need to declare both. The true and false elements should hold one\nhtml element at the minimum.\n\nThe most simple way to use VCheck is to refer to a component field:\n\n`my-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'my-component', \n  styles: [], \n  html: `\n    \u003cv-check if=\"{{ isLoggedIn }}\"\u003e\n      \u003ctrue\u003e\n        \u003cspan\u003eYou are logged in!\u003c/span\u003e\n      \u003c/true\u003e\n      \u003cfalse\u003e\n        \u003cspan\u003eYou are not logged in!\u003c/span\u003e\n      \u003c/false\u003e\n    \u003c/v-check\u003e\n  `\n})\nexport class MyComponent { \n  isLoggedIn = true; \n}\n\n```\n\nIt is also possible to perform a method call prior to rendering:\n\n`my-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'my-component', \n  styles: [], \n  html: `\n    \u003cv-check if=\"isLoggedIn()\"\u003e\n      \u003ctrue\u003e\n        \u003cspan\u003eYou are logged in!\u003c/span\u003e\n      \u003c/true\u003e\n      \u003cfalse\u003e\n        \u003cspan\u003eYou are not logged in!\u003c/span\u003e\n      \u003c/false\u003e\n    \u003c/v-check\u003e\n  `\n})\nexport class MyComponent { \n  isLoggedIn(): boolean { \n    return true; \n  } \n}\n\n```\n\n### VRepeat\n\nWith VRepeat, you can easily multiply elements. It is also possible to use a variable for every element that you render.\nFor instance:\n\n`my-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'my-component', \n  styles: [], \n  html: `\n      \u003cv-repeat let=\"{{ i }}\" for=\"['first', 'second', 'third']\"\u003e\n        \u003cspan\u003e{{ i }}\u003c/span\u003e\n      \u003c/v-repeat\u003e\n  `\n})\nexport class MyComponent {}\n\n```\n\nIt is also possible to make use of a component array:\n\n`my-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'my-component', \n  styles: [], \n  html: `\n    \u003cv-repeat let=\"{{ person }}\" for=\"{{ persons }}\"\u003e\n      \u003cspan\u003e{{ person.name }}\u003c/span\u003e\n      \u003cspan\u003e{{ person.age }}\u003c/span\u003e\n    \u003c/v-repeat\u003e\n  `\n})\nexport class MyComponent { \n  persons = [\n    { name: 'Bert', age: 30 }, \n    { name: 'Ernie', age: 60 }\n  ]; \n}\n\n```\n\n### VSwitch\n\nIn some cases, you might want to use a switch statement in your views. For this, you can use `VSwitch`. Important: you\nmust\neither have a matching case for your statement, or a default case! If one is missing, the switch will not work, since\nVienna does not know what to display in that particular situation.\n\n```\n@VComponent({\n    selector: 'switch-component',\n    styles: [],\n    html: `\n        \u003cv-switch condition=\"{{ name }}\"\u003e\n            \u003cv-case if=\"admin\"\u003e\n                May the force be with you!\n            \u003c/v-case\u003e\n            \u003cv-case if=\"member\"\u003e\n                You have limited force here...\n            \u003c/v-case\u003e\n            \u003cv-case-default\u003e\n                You have no force here.\n            \u003c/v-case-default\u003e\n        \u003c/v-switch\u003e\n    `\n})\nexport class SwitchComponent {\n    name = 'admin';\n}\n```\n\n## Event binding\n\nVienna is created to create web applications with ease. Of course, event binding is supported in the Vienna template\nengine. To listen to an event on one element, just add the @mark. For instance:\n\n`custom.component.ts`\n\n```\n\n@VComponent({ \n  selector: 'custom-component', \n  styles: [], \n  html: `\u003cbutton @click='showAlert('Hello!')\u003eShow alert\u003c/button\u003e`\n})\nexport class CustomComponent { \n  showAlert(message: string): void { \n    alert(message); \n  }\n}\n\n```\n\nIf you want to listen to keyboard events, you can pass the key name. This holds for `keyDown` and `keyUp`.\n\n```\n\u003cdiv @keyDown.enter=\"doSomething\"\u003e\u003c/div\u003e\n```\n\n## Pipes\n\nPipes are ways to transform template values. It is possible to chain multiple pipes, such as:\n\n```\n {{ variable | pipe1 | pipe2 }}\n```\n\n\u003cstrong\u003eImportant:\u003c/strong\u003e order is important when you chain pipes since values will be transformed in one pipe, and\nthen passed to another one. If you do not respect order, you might end up with invalid values.\n\n### Raw\n\nWith the raw pipe, values are not checked for xss attacks. As a consequence, the html is not stripped, which\nmay\nbe handy from time to time. However, printing raw data in html is always a security risk, so keep that in mind.\n\n```\n@VComponent({\n    selector: 'pipe-component',\n    styles: [],\n    html: `{{ html | raw }}`\n})\nexport class PipeComponent {\n    html = `\n        \u003cdiv class=\"container\"\u003e\n            \u003cp\u003eLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu augue ut lectus arcu bibendum at varius. Vitae nunc sed velit dignissim sodales ut eu sem. Varius sit amet mattis vulputate enim nulla aliquet porttitor lacus. Id eu nisl nunc mi ipsum faucibus. Iaculis at erat pellentesque adipiscing commodo. Ut venenatis tellus in metus vulputate. Venenatis cras sed felis eget velit. Lacinia quis vel eros donec ac odio tempor orci. Non pulvinar neque laoreet suspendisse interdum.\u003c/p\u003e\n            \u003cp\u003eNisl suscipit adipiscing bibendum est ultricies. Cras pulvinar mattis nunc sed blandit libero volutpat sed cras. Sed libero enim sed faucibus turpis in eu. Duis convallis convallis tellus id interdum velit laoreet id. Et malesuada fames ac turpis egestas sed tempus. In vitae turpis massa sed elementum. Sit amet consectetur adipiscing elit duis tristique. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Pharetra et ultrices neque ornare aenean euismod elementum nisi. Dictumst vestibulum rhoncus est pellentesque elit. Risus ultricies tristique nulla aliquet enim tortor at. Risus nullam eget felis eget nunc lobortis mattis aliquam faucibus. Massa placerat duis ultricies lacus. Tellus at urna condimentum mattis pellentesque id nibh. Erat velit scelerisque in dictum non consectetur a erat nam. Egestas egestas fringilla phasellus faucibus scelerisque eleifend.\u003c/p\u003e\n        \u003c/div\u003e\n    `;\n}\n```\n\n### Json\n\nWith the json pipe, one is able to print the template variable as json string.\n\n```\n@VComponent({\n    selector: 'pipe-component',\n    styles: [],\n    html: `{{ html | json }}`\n})\nexport class PipeComponent {\n    html = `\n        \u003cdiv class=\"container\"\u003e\n            \u003cp\u003eLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eu augue ut lectus arcu bibendum at varius. Vitae nunc sed velit dignissim sodales ut eu sem. Varius sit amet mattis vulputate enim nulla aliquet porttitor lacus. Id eu nisl nunc mi ipsum faucibus. Iaculis at erat pellentesque adipiscing commodo. Ut venenatis tellus in metus vulputate. Venenatis cras sed felis eget velit. Lacinia quis vel eros donec ac odio tempor orci. Non pulvinar neque laoreet suspendisse interdum.\u003c/p\u003e\n            \u003cp\u003eNisl suscipit adipiscing bibendum est ultricies. Cras pulvinar mattis nunc sed blandit libero volutpat sed cras. Sed libero enim sed faucibus turpis in eu. Duis convallis convallis tellus id interdum velit laoreet id. Et malesuada fames ac turpis egestas sed tempus. In vitae turpis massa sed elementum. Sit amet consectetur adipiscing elit duis tristique. Aliquam sem fringilla ut morbi tincidunt augue interdum velit. Pharetra et ultrices neque ornare aenean euismod elementum nisi. Dictumst vestibulum rhoncus est pellentesque elit. Risus ultricies tristique nulla aliquet enim tortor at. Risus nullam eget felis eget nunc lobortis mattis aliquam faucibus. Massa placerat duis ultricies lacus. Tellus at urna condimentum mattis pellentesque id nibh. Erat velit scelerisque in dictum non consectetur a erat nam. Egestas egestas fringilla phasellus faucibus scelerisque eleifend.\u003c/p\u003e\n        \u003c/div\u003e\n    `;\n}\n```\n\n### Encode base64\n\nBase64 encode a template value is easy by passing the `encodeBase64` pipe:\n\n```\n@VComponent({\n    selector: 'pipe-component',\n    styles: [],\n    html: `{{ value | encodeBase64 }}`\n})\nexport class PipeComponent {\n    value = 'Hello World';\n}\n```\n\n### Decode base64\n\nTo decode a template value, just pass the `decodeBase64` pipe:\n\n```\n@VComponent({\n    selector: 'pipe-component',\n    styles: [],\n    html: `{{ value | decodeBase64 }}`\n})\nexport class PipeComponent {\n    value = 'SGVsbG8gV29ybGQ=';\n}\n```\n\n### Create custom pipes\n\nVienna supports custom pipes. In order to create a custom pipe, add the `VPipe` decorator accordingly. Also, implement\nthe `VPipeTransform` interface.\n\nFor example:\n\n```\n@VPipe({ name: 'greeting' })\nexport class GreetingPipe implements VPipeTransform {\n    transform(value: string): string {\n        return `${value}, world!`;\n    }\n}\n```\n\nSecondly, register your pipe on application level:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [HomeComponent],\n    routes: [\n      { path: '/', component: HomeComponent }\n    ],\n    pipes: [GreetingPipe]\n})\nexport class Application {}\n```\n\n\u003cstrong\u003eImportant:\u003c/strong\u003e all pipes should have different names. Vienna does not accept duplicate pipe names since it\ndoes not know which pipe to use in that specific case.\n\nThen, add your pipe to the template reference:\n\n```\n@VComponent({\n    selector: 'home-component',\n    styles: [],\n    html: `{{ myGreeting | greeting }}`\n})\nexport class HomeComponent {\n    myGreeting = 'Hello';\n}\n```\n\nThe pipe above should print `Hello, world!`.\n\n## Routes\n\nIn order to use your Vienna application, you need to specify routes. With these routes, Vienna knows which components it\nshould render for the corresponding paths.\n\nTo create a route, just add it to the `VApplication` decorator. The path and component properties are mandatory. For\ninstance:\n\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [HomeComponent, AboutComponent], \n  routes: [\n    { path: '/', component: HomeComponent }, \n    { path: '/about', component: AboutComponent }\n  ]\n})\nexport class Application {}\n\n```\n\nBesides the path and component properties, the `VRoute` interface accepts the following optional values:\n\n- `data` (optional): key-value based map to specify some custom values for that specific route.\n- `guards` (optional): implementations of the `VRouteGuard` interface that allow you to control the accessibility of a\n  route based on a custom condition.\n- `children` (optional): an array of `VRoute` objects that represent nested routes.\n- `docTags` (optional): an array of `VRouteDocTag` objects, which are needed for seo optimization.\n\n### Nested routes\n\nRoutes can be nested limitless. To create subroutes, just implement child routes. For example:\n\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [\n    HomeComponent, \n    AboutComponent, \n    ContactComponent\n  ], \n  routes: [\n    { path: '/', component: HomeComponent }, \n    { \n      path: '/about', \n      component: AboutComponent,\n      children: [\n        { path: '/contact', component: ContactComponent }, \n      ]\n    }\n  ]\n})\nexport class Application {}\n\n```\n\n### Route wildcards\n\nThe Vienna router supports wildcards. Just pass the `*`-sign to add a wildcard. Note that the first route wins. This\nmeans\nthat if you have two routes that match, the router picks the first one. It does not matter whether one of them is a\nwildcard.\n\n```\n\n@VApplication({ \n  declarations: [\n    AboutComponent,  \n    PageNotFoundComponent\n  ], \n  routes: [\n    { \n      path: '/about', \n      component: AboutComponent\n    },\n    { \n      path: '*', \n      component: PageNotFoundComponent\n    }\n  ]\n})\nexport class Application {}\n\n```\n\nPlease note: the example above is just to demonstrate how you can use the wildcard. Of course, if you want to render a\ncomponent\nfor a page that is not found, you can also implement the `routeNotFoundStrategy`.\n\n### Route data\n\nOptional key-value based map to specify some custom values for a specific route.\n\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [HomeComponent], \n  routes: [\n    { \n      path: '/', \n      component: HomeComponent, \n      data: { titleOverride: 'My custom title override' } \n    }\n  ]\n})\nexport class Application {}\n\n```\n\nIn order to access the route data, you need to inject the `VActivatedRoute` in the constructor of your component:\n\n`home-component.ts`\n\n```\n\n@VComponent({ \n  selector: 'home-component', \n  styles: [], \n  html: `\u003cspan\u003e{{ title }}\u003c/span\u003e`\n})\nexport class HomeComponent {\n    title = '';\n    \n    constructor(private activatedRoute: VActivatedRoute) {\n        this.activatedRoute.data(data =\u003e this.title = data.titleOverride);\n    }\n}\n\n```\n\n### Route guards\n\nOptional implementations of the `VRouteGuard` interface that allow you to control the accessibility of a route based on\na custom condition.\n\n`cookie.guard.ts`\n\n```\n\nexport class CookieGuard implements VRouteGuard {\n    guard(): boolean {\n        return sessionStorage.getItem('allowedToUseCookies') === 'true';\n    }\n}\n\n```\n\nThen, add your guard to the route that you want to control:\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [HomeComponent, ComponentWithAnalytics], \n  routes: [\n    { path: '/', component: HomeComponent }, \n    { path: '/lorem', component: ComponentWithAnalytics, guards: [CookieGuard] },\n  ]\n})\nexport class Application {}\n\n```\n\nA guard can make use of the current route, by adding the `VRoute` as argument. For instance:\n\n`authorized.guard.ts`\n\n```\n\n@VInjectable()\nexport class AuthorizedGuard implements VRouteGuard {\n    constructor(protected loginService: LoginService) {}\n\n    guard(route: VRoute): boolean {\n        const userRole = this.loginService.role;\n        const authorizedForRole = route.data.authorizedForRole;\n        return userRole \u0026\u0026 authorizedForRole \u0026\u0026 userRole === authorizedForRole\n    }\n}\n\n```\n\nPlease note that this guard uses the `VInjectable` decorator for dependency injection. Without this decorator, we are\nunable to inject the LoginService. Please see dependency injection for more information.\n\n\u003cb\u003eImportant:\u003c/b\u003e if the guard returns false for some reason, the internal Vienna router handles the current route the\nsame as a route that isn't found. In that case, the so-called\n`routeNotFoundStrategy` kicks in. You might want to adjust this strategy depending on your needs.\n\n### Route params\n\nConsider the following url `#/blog/1` with the following route signature `#/blog/:id`. In order to retrieve the route\nparam 'id', you can\nuse the `VActivatedRoute` to retrieve a list of current `VRouteParam` values.\n\nA `VRouteParam` holds the following values:\n\n- `id`: the name of the route param name\n- `value`: the actual route param value\n\n`blog-post.component.ts`\n\n```\n\n@VComponent({ \n  selector: 'blog-post-component', \n  styles: [], \n  html: `\u003cspan\u003e{{ id }}\u003c/span\u003e` // prints '1'\n})\nexport class BlogPostComponent {\n    id = '';\n    \n    constructor(private activatedRoute: VActivatedRoute) {\n        this.activatedRoute.params(params =\u003e this.id = params.find(v =\u003e v.id === 'id'));\n    }\n}\n\n```\n\nIn order to add route params to Vienna, just pass them in your routes. For example:\n\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [HomeComponent, BlogComponent, BlogPostComponent], \n  routes: [\n    { path: '/', component: HomeComponent }, \n    { \n      path: '/blog', \n      component: BlogComponent,\n      children: [\n        { path: '/:id', component: BlogPostComponent, guards: [BlogPostIdGuard] }\n      ]\n    },\n  ]\n})\nexport class Application {}\n\n```\n\n### Query params\n\nConsider the following url: `#/dashboard?message=Hello%20there`. In order to retrieve the query param 'message', you can\nuse the `VActivatedRoute`:\n\n`dashboard.component.ts`\n\n```\n\n@VComponent({ \n  selector: 'dashboard-component', \n  styles: [], \n  html: `\u003cspan\u003e{{ welcomeMsg }}\u003c/span\u003e` // prints 'Hello there'\n})\nexport class DashboardComponent {\n    welcomeMsg = '';\n    \n    constructor(private activatedRoute: VActivatedRoute) {\n        this.activatedRoute.queryParams(params =\u003e this.welcomeMsg = params.message);\n    }\n}\n\n```\n\n### Route redirects\n\nIt is very likely that an application needs to redirect to another internal or external path. Of course, it is possible\nto provide\na valid href in the view. However, sometimes you want to create a redirect that is not visible in your view. For this,\nyou can use the\n`VRouteRedirect` helper class.\n\nThe VRouteRedirect supports to redirect options:\n\n- `redirectTo`: redirects to an internal or external path. In order to navigate to another Vienna path, just provide the\n  prefix '#' (e.g. `#/my-vienna-path`). Of course, you can navigate to an external domain as well. Just pass the\n  complete url as-is (e.g. `https://www.some-external-site.com`). Optionally, you can specify whether you want to open\n  the link in another window.\n- `redirectToRoot`: just navigates to the root component of the Vienna app (of course, you must have a valid root path\n  for that).\n\nThe VRouteRedirect is an injectable, which means that you can inject it in your constructor:\n\n`custom.component.ts`\n\n```\n\n@VComponent({ \n  selector: 'custom-component', \n  styles: [], \n  html: ``\n})\nexport class CustomComponent implements VInit { \n  \n  constructor(private routeRedirect: VRouteRedirect) {}\n  \n  vInit(): void {\n    setTimeout(() =\u003e this.routeRedirect.redirectTo('#/another-vienna-path', true), 2000)); // 'true' means new window here\n  }\n}\n\n```\n\n### Seo optimization\n\nSince Vienna is in the end a framework that runs client side, it may be difficult for web crawlers to scan Vienna pages\nfor content. Vienna can help these crawlers by injecting the necessary title and meta tags in the document, since\nthese crawlers heavily rely on these tags. This will improve the seo of the application.\n\nTo use the document tags, just add them to your route. If you have nested routes, the most specific route 'wins'.\nHence, the tags of the most specific route are added to the dom, while the tags of the parents are ignored.\n\n`application.ts`\n\n```\n\n@VApplication({ \n  declarations: [\n    HomeComponent\n  ], \n  routes: [\n    { \n      path: '/', \n      component: HomeComponent,\n      docTags: {\n        title: 'My fancy home title',\n        meta: [{ name: 'author', content: 'Lucky Luke' }]\n      }\n    }\n  ]\n})\nexport class Application {}\n\n```\n\nIf you want to create document tags inside the component itself, because, for instance, you need some specific data,\njust use the [override method](#override-document-tags) from the `VWeb` utility.\n\n## Dependency injection\n\nVienna provides a basic implementation of dependency injection, that uses reflection under the hood. In order to inject\na dependency, like a service, in another class, like a component, you need to provide the `VInjectable` decorator:\n\n`custom.service.ts`\n\n```\n\n@VInjectable()\nexport class CustomService {\n  getHelloWorld(): string { \n    return 'Hello world!'; \n  } \n}\n\n```\n\n`custom.component.ts`\n\n```\n\n@VComponent({ \n  selector: 'custom-component', \n  styles: [], \n  html: `\u003cspan\u003e{{ message }}\u003c/span\u003e` // prints 'Hello world!'\n})\nexport class CustomComponent {\n    message = '';\n    \n    constructor(protected customService: CustomService) {\n        this.message = customService.getHelloWorld();\n    }\n}\n\n```\n\nBy default, all injectable classes are singleton. If you do not want this for some reason, you can create a fresh\ninstance for every injection. For this, just set singleton to false:\n\n````\n\n@VInjectable({ singleton: false })\nexport class CustomService {...}\n\n````\n\n## Dark mode\n\nNowadays, dark mode is very convenient to add some additional usability features to your application. Some people like\ndark apps more than light apps, and for some it is even a necessity to use an app.\n\n### Set up dark mode\n\nLuckily, Vienna comes with dark mode out of the box. There are two ways to enable dark mode. The easiest way is to set\nup dark mode in your application config by using the so-called `isDarkModeEnabled` hook. It is just a method that should\nreturn true or false. If it returns true, dark mode will be enabled for the whole application. For instance:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [],\n    routes: [],\n    darkMode: {\n      isDarkModeEnabled: () =\u003e {\n        // ... Some custom logic here. \n        return true; \n      }\n    }\n})\nexport class Application {}\n```\n\nIf dark mode is enabled, by default, Vienna will apply a `v-dark` css class to all elements in the dom. Then, you have\nvarious possibilities to style the dark selectors. For instance, just inject some global styling to set some default for\nthe v-dark class (e.g. a specific background color and specific text color). Or, you can apply custom css rules for all\nyour Vienna components separately.\n\nVienna is flexible, though. For instance, you can apply dark mode initially, but later disable it. Or, you can disable\ndark mode by default, and manually enable it later. To do this, you only need to inject the `VDarkMode` helper\nsomewhere:\n\n```\n@VComponent({\n    selector: 'dark-mode-component',\n    html: `\n        \u003cdiv class='background'\u003e\n            \u003ch2\u003eSome title\u003c/h2\u003e\n            \n            \u003cv-check if=\"{{ isDarkModeEnabled }}\"\u003e\n                \u003ctrue\u003e\n                    \u003cbutton @click=\"disableDarkMode()\"\u003eDisable dark mode\u003c/button\u003e\n                \u003c/true\u003e\n                \u003cfalse\u003e\n                    \u003cbutton @click=\"enableDarkMode()\"\u003eEnable dark mode\u003c/button\u003e\n                \u003c/false\u003e\n            \u003c/v-check\u003e\n        \u003c/div\u003e`,\n    styles: [`\n        .v-dark {\n            background-color: #000;\n            color: #fff;\n        }\n    `]\n})\nexport class DarkModeComponent implements VInit {\n\n    isDarkModeEnabled = false;\n\n    constructor(private darkMode: VDarkMode) {}\n\n    vInit(): void {\n        this.isDarkModeEnabled = this.darkMode.isDarkModeEnabled();\n    }\n\n    enableDarkMode(): void {\n        this.darkMode.enableDarkMode();\n    }\n\n    disableDarkMode(): void {\n        this.darkMode.disableDarkMode();\n    }\n}\n```\n\n### Customize dark mode\n\nIf you want to customize the `v-dark` class for some reason, you can choose between an application-wide class set up of\na component override. To apply the application-wide option, just pass the `darkModeClassOverride` to your application\nconfig:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [],\n    routes: [],\n    darkMode: {\n      isDarkModeEnabled: () =\u003e {...},\n      darkModeClassOverride: 'my-custom-dark-mode-class'\n    }\n})\nexport class Application {}\n```\n\nOr if you want to specify a custom class in you component, apply the `darkModeClassOverride`:\n\n```\n@VComponent({\n    selector: 'dark-mode-component',\n    html: ...,\n    styles: [...],\n    darkModeClassOverride: 'my-custom-dark-mode-class'\n})\nexport class DarkModeComponent implements VInit {...}\n```\n\n\u003cb\u003eImportant:\u003c/b\u003e a Vienna component option is always more specific than some application-wide config. Therefore, if you\napply dark mode settings to a component, these settings will always be true. For instance, if you set up a custom dark\nmode class globally, and have another custom class in your component, the latter will be used.\n\n## Validator\n\nVienna provides a simple, extendable validation engine out of the box. With this, you are able to validate objects with\nease.\n\nJust inject the validator, which is an injectable. Then, define the fields you want to validate, and the functions you\nwant to\nuse for the validation. It is also possible to select nested fields. The result is a wrapper object that holds methods\nto verify the result.\n\nVienna provides the following simple validator functions for you:\n\n- `vNoBlankValidator`: checks whether a value is not null, is defined, and has length \u003e= 1\n- `vStringValidator`: checks whether a value is a string\n- `vNumberValidator`: checks whether a value is a number\n- `vLengthValidator`: checks whether the value is a string and has a certain required length\n\n```\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  private readonly _person = {\n    name: 'Ernie',\n    age: 30\n  };\n\n  constructor(private _validator: VValidator) {}\n\n  validate(): void {\n    const result = this._validator.validate(this._person, [\n      { fields: ['name', 'age'], functions: [vNoBlankValidator()] },\n      { fields: ['name'], functions: [vStringValidator(), vLengthValidator(4)] },\n      { fields: ['age'], functions: [vNumberValidator()] }\n    ]);\n    \n    console.log(result.isValid()); // false\n    console.log(result.errorSize()); // 1\n    console.log(result.errors()[0].cause); // 'length error'\n  }\n}\n```\n\nOf course, you can easily extend the validation engine by passing your own validation functions. Just implement the\n`VValidationFunction` interface.\n\n```\nclass MyCustomEmailValidator implements VValidationFunction {   \n    validate(value: any): VInternalValidationError[] {\n      if (this.isValidEmail(value)) {\n        return [];\n      } else {\n        return [{ cause: 'no email', message: `Value '${value}' is no email!` }];\n      }\n    }\n    \n    private isValidEmail(value: string): boolean {...}\n}\nconst myCustomEmailValidator = () =\u003e new MyCustomEmailValidator();\n\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  private readonly _person = {\n    name: 'Ernie',\n    age: 30,\n    contact: { email: 'ernie@someplace.com', telephone: '+312345678' }\n  };\n\n  constructor(private _validator: VValidator) {}\n\n  validate(): void {\n    const result = this._validator.validate(this._person, [\n      { fields: ['contact.email'], functions: [myCustomEmailValidator()] }\n    ]);\n  }\n}\n```\n\n## I18n\n\n### Global i18n configuration\n\nWhen you go international, you might want your app to be multilingual. For this, you can use the Vienna i18n support.\n\nFirst, create one or more `VI18nLanguageSet`, such as:\n\n```\nconst ENGLISH_LANG: VI18nLanguageSet = {\n  name: 'en',\n  translations: {\n    'greeting': 'Hello, world!'\n  }\n};\n\nconst DUTCH_LANG: VI18nLanguageSet = {\n  name: 'nl',\n  translations: {\n    'greeting': 'Hallo, wereld!'\n  }\n};\n```\n\nThen, set up a so-called i18n config in your application and implement the `setActiveLanguageSet` method. This method\nneeds to return one `VI18nLanguageSet`. Of course, you are free to implement your own logic.\n\n`application.ts`\n\n```\n@VApplication({\n    ...\n    i18n: {\n      setActiveLanguageSet: () =\u003e {\n        // Just a simple implementation for demo purposes\n        if (window.location.href.endsWith('?lang=nl')) {\n            return DUTCH_LANG;\n        } else {\n            return ENGLISH_LANG;\n        }\n      }\n    }\n})\nexport class Application {}\n```\n\nThen, implement the language key somewhere in your component. Mind the two %-signs at the beginning and the end, which\nare required for i18n values. For instance:\n\n```\n@VComponent({\n    selector: 'i18n-component',\n    styles: [],\n    html: `{{ %greeting% }}`\n})\nexport class I18nComponent {}\n```\n\nIn the example above, when the page is called with `?lang=nl`, the component will show 'Hallo, wereld!'. Otherwise, it\nwill\nshow 'Hello, world!'.\n\n### Dynamic i18n configuration\n\nOf course, you can also create an i18n value dynamically. Just implement the `VI18n` injectable. For example:\n\n```\n@VComponent({\n    selector: 'i18n-component',\n    styles: [],\n    html: `\n        \u003cp\u003e{{ text }}\u003c/p\u003e\n        \u003cbutton @click=\"changeValue()\"\u003eReplace text\u003c/button\u003e\n    `\n})\nexport class I18nComponent {\n    text = 'Some placeholder';\n\n    constructor(private _i18n: VI18n) {\n    }\n\n    changeValue(): void {\n        this.text = this._i18n.findTranslation('greeting');\n    }\n}\n```\n\nThe VI18n calls the `setActiveLanguageSet` logic in your application config to determine which language set to use.\nTherefore, make sure you have implemented your logic, otherwise the value will not be translated.\n\n## Miscellaneous\n\nVienna provides various handy miscellaneous tools that you can use.\n\n### VAudit\n\nVienna provides a data validator class that you can use to verify multiple data fields. Currently, the following\nmethods are supported:\n\n- `isValidUrl` (http or https is required)\n- `isValidEmail`\n- `isValidIp4`\n- `isValidIp6`\n- `isBlank`\n- `isUserAgentBot`\n- `isUserAgentAi` (currently detecting gpt, claude, mistral, oai, google-extended, perplexity and others)\n\nThe Vienna data validator is called `VAudit`, and is an injectable. Hence, just inject it in your code:\n\n```\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  constructor(private _audit: VAudit) {}\n\n  vInit(): void {\n    const url = 'https://www.my-url.com';\n    this._audit.isValidUrl(url); // true\n  }\n}\n```\n\n### VWeb\n\nVienna provides basic web utilities, which are all available under the so-called `VWeb` class. This class is an\ninjectable.\n\n#### Slugify\n\nFor creating slugs. You can pass the following optional options: `trim` and `toLowerCase`.\n\n```\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  constructor(private _web: VWeb) {}\n\n  vInit(): void {\n    const value = 'my string';\n    this._web.slugify(value); // 'my-string'\n  }\n}\n```\n\n#### Cookies\n\nCookies let you store user information in web pages. Vienna helps you with this by providing an easy api for storing,\nretrieving and removing cookies. For this, Vienna uses the fully-tested and commonly\nused [js-cookie](https://github.com/js-cookie/js-cookie) library\nunder the hood. In this case, Vienna acts as simple proxy.\n\n```\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  constructor(private _web: VWeb) {}\n\n  vInit(): void {\n    // Set cookie\n    this._web.setCookie('cookieName', 'cookieValue');\n    \n    // Set cookie with options\n    this._web.setCookie('cookieName', 'cookieValue', { secure: true });\n    \n    // Read cookie\n    this._web.getCookie('cookieName'); // 'cookieValue'\n    \n    // Remove cookie\n    this._web.removeCookie('cookieName');\n  }\n}\n```\n\n#### Override document tags\n\nVWeb provides a method to override document tags that are set in the Vienna route as [VRouteDocTag](#seo-optimization).\nThis may be helpful in situations where you want to create dynamic titles (e.g. correspondig to a specific blog post).\n\nIn order to use this simple utility method, you need to call it inside the `VAfterInit` hook, since document tags in the\ncurrent\nroute tree are called first.\n\n```\n@VComponent({...})\nexport class MyComponent implements VAfterInit {\n\n  const post = {\n    title: 'Blogpost title',\n    body: 'Some body',\n    author: 'Lucky Luke'\n  };\n\n  constructor(private _web: VWeb) {}\n\n  vAfterInit(): void {\n    this._web.overrideTags({\n      title: this.post.title,\n      meta: [ { name: 'author', content: this.post.author } ]\n    });\n  }\n}\n```\n\n## Plugins\n\nPlugins are pieces of optional utilities that you can configure inside Vienna. They are not mandatory, because they do\nnot\nbelong to the core of the Vienna framework. However, they might be handy to use.\n\n### Logger\n\nVienna ships a basic logger implementation that you can configure to send messages to an external logging provider, such\nas Sentry.\nTo get started with the logger, just configure the plugin in your application config:\n\n`application.ts`\n\n```\n@VApplication({\n    declarations: [],\n    routes: [],\n    plugins: {\n      logger: {\n          process: logs =\u003e // ... your implementation (e.g. send logs to some external provider)\n      }\n    }\n})\nexport class Application {}\n```\n\nEverytime a log line is recorded, the process method will be called. To use the logger, just inject it:\n\n```\n@VComponent({...})\nexport class MyComponent implements VInit {\n\n  constructor(private _logger: VLogger) {}\n\n  vInit(): void {\n    this._logger.info('Some info log');\n  }\n}\n```\n\nThe output of the `process` method will be:\n\n```\n[{ type: 'info', msg: 'Some info log' }]\n```\n\n## Component testing\n\nVienna is still under construction. However, there is a basic testbed available to test Vienna components with ease:\n\n```\n@VComponent({\n    selector: 'custom-component',\n    styles: [' :host { margin: 0; padding: 0; }'],\n    html: `\u003cspan\u003e{{ message }}\u003c/span\u003e\u003cspan\u003eLorem Ipsum\u003c/span\u003e`\n})\nexport class CustomComponent {\n    message = 'Some text'\n}\n\ndescribe('VComponentFactory', () =\u003e {\n\n    let component: VTestComponent\u003cCustomComponent\u003e;\n\n    beforeEach(() =\u003e {\n        const createComponent = vComponentFactory\u003cCustomComponent\u003e({\n            component: CustomComponent,\n        });\n        component = createComponent();\n    });\n\n    it('should have valid styles', () =\u003e {\n        expect(component.styles).toEqual('body { padding: 0; margin: 0; } :host { margin: 0; padding: 0; }');\n    });\n\n    it('should have valid html', () =\u003e {\n        expect(component.html).toEqual('\u003cspan\u003eSome text\u003c/span\u003e\u003cspan\u003eLorem Ipsum\u003c/span\u003e');\n    });\n\n    it('should have valid contents', () =\u003e {\n        expect(component.query('span').innerHTML).toEqual('Some text');\n        expect(component.queryAll('span')).toHaveLength(2);\n        expect(component.queryAll('span')[1].innerHTML).toEqual('Lorem Ipsum');\n    });\n})\n```\n\n# Todo\n\n- Add renderer cache to increase rendering performance (e.g. use render event for one component + internal component id\n  instead of all)\n- Add unit tests\n- Use Rollup instead of Webpack since it generates a much smaller bundle size\n\n# Known issues\n\n- Event listener (such as click) may stop intervals and timeouts in same component.\n- VProp is leaking state when routing to subcomponent that has input binding.\n- VInit does not work without callback yet.\n- Running tests with vComponentFactory may cause Jest open handles issue. As a workaround, you can enable fake timers in\n  the Jest config or explicitly disable real timers in your tests (do not forget to re-enable them!).\n- Events may not work in child components, investigation needed.\n- Pipes only work on component top level; pipes do not work inside inner template refs yet.\n\n# Literature\n\n- https://www.thinktecture.com/en/web-components/flaws/\n- https://hackernoon.com/how-to-create-new-template-engine-using-javascript-8f26313p\n- https://www.raresportan.com/object-change-detection/ (using Proxy)\n- https://fatfreeframework.com/3.7/views-and-templates (nice example of directive syntax)~~\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoerinijs%2Fvienna","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyoerinijs%2Fvienna","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoerinijs%2Fvienna/lists"}