{"id":19877235,"url":"https://github.com/rohiievych/swayer","last_synced_at":"2025-05-02T12:30:44.687Z","repository":{"id":65838158,"uuid":"595821182","full_name":"rohiievych/swayer","owner":"rohiievych","description":"JavaScript-only UI engine for fast and low overhead development","archived":false,"fork":false,"pushed_at":"2024-10-13T20:48:21.000Z","size":1079,"stargazers_count":18,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-07T01:23:10.966Z","etag":null,"topics":["engine","framework","frontend","javascript","schema","ui"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rohiievych.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"patreon":"rohiievych"}},"created_at":"2023-01-31T21:55:15.000Z","updated_at":"2025-03-22T10:44:53.000Z","dependencies_parsed_at":null,"dependency_job_id":"edb70e6e-c50a-4b03-959d-343038764a48","html_url":"https://github.com/rohiievych/swayer","commit_stats":{"total_commits":16,"total_committers":1,"mean_commits":16.0,"dds":0.0,"last_synced_commit":"705ce607610360ef0c55f4eec2fe96a38efa3074"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rohiievych%2Fswayer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rohiievych%2Fswayer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rohiievych%2Fswayer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rohiievych%2Fswayer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rohiievych","download_url":"https://codeload.github.com/rohiievych/swayer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252038095,"owners_count":21684624,"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":["engine","framework","frontend","javascript","schema","ui"],"created_at":"2024-11-12T16:36:54.391Z","updated_at":"2025-05-02T12:30:44.366Z","avatar_url":"https://github.com/rohiievych.png","language":"JavaScript","funding_links":["https://patreon.com/rohiievych"],"categories":[],"sub_categories":[],"readme":"# Swayer - schema based UI engine\n\n[![npm version](https://img.shields.io/npm/v/swayer)](https://www.npmjs.com/package/swayer)\n[![npm downloads/month](https://img.shields.io/npm/dm/swayer.svg)](https://www.npmjs.com/package/swayer)\n[![npm downloads](https://img.shields.io/npm/dt/swayer.svg)](https://www.npmjs.com/package/swayer)\n[![snyk](https://snyk.io/test/github/rohiievych/swayer/badge.svg)](https://snyk.io/test/github/rohiievych/swayer)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/rohiievych/swayer/blob/main/LICENSE)\n\n**JavaScript-only UI engine** for fast and low overhead development\n\n## Before we begin\n\nFind your way to help ukrainians defend the world of freedom and democracy:\n\n- Support Ukraine at [United24](https://u24.gov.ua/)\n- Support [Ukrainian Armed Forces](https://bank.gov.ua/en/news/all/natsionalniy-bank-vidkriv-spetsrahunok-dlya-zboru-koshtiv-na-potrebi-armiyi)\n- Make a donation to [Come Back Alive](https://savelife.in.ua/en/donate-en) charity foundation\n- Find more donation targets at [Stand For Ukraine](https://standforukraine.com/)\n\n**Why is this so important?**\n\nMy home country has been ruined by terrible war started by Russian Federation. Thousands of people lost their lives and homes. Millions are affected by acts of terrorism, which occur on daily basis. They say they came to protect someone, but instead cruelly kill children and elderly people, erase whole cities from the face of the earth, because they simply don't want Ukraine to exist. This is insane to have such reality in the 21st century, so we must stop the global evil from expanding. Arm Ukraine - give this world a chance!\n\n**This technological product is prohibited for use by citizens of #RussiaIsATerroristState.**\n\n## Quick start\n\nInstall starter project with Swayer CLI\n\n```shell\nnpx swayer create mySwayerProject\n```\n\nChange directory to newly created project\n\n```shell\ncd mySwayerProject\n```\n\nStart application\n\n```shell\nnpm start\n```\n\n## Showcase\n\n- See online [Todo Application demo](https://rohiievych.github.io/swayer/todo-app)\n- Play with [example](https://github.com/rohiievych/swayer/tree/main/examples/todo-app) code to investigate how it works\n\n## Contacts\n\nSwayer Telegram community:\n  - channel: [Swayer](https://t.me/SwayerEngine)\n  - chat: [Swayer Chat](https://t.me/SwayerChat)\n\nE-mail: [roman@swayer.dev](mailto:roman@swayer.dev)\n\n## Description\n\nSwayer is a comprehensive user interface engine, which enables pure **JavaScript to describe document structure, styling and behavior with no need to write HTML and CSS code**. This instrument is provided for creating rich web applications using the power of JavaScript with all its capabilities out-of-the-box. You can build a wide range of application types from simple static site, which can be rendered even from JSON data, to stateful single page applications with complex business logic.\n\n#### UI Engine vs UI Framework vs UI Library\n\nThe difference between these types of software is a responsibility scope. You import a library to help you with something, e.g. rendering, while a framework provides a generic structure reducing amount of trivial work. UI engine is designed to encapsulate a powerful logic, which performs a task. In case of Swayer, **it consumes component schemas and outputs dynamic JavaScript application hiding lots of complexities under the hood.**\n\n#### Why not to stick with hybrid syntax like JSX?\n\nThe answer is simple - you still play with HTML syntax, which has to be parsed before it can be processed with JavaScript. That's a quite big overhead. While HTML syntax is really well known - it was created for describing static web documents, not interactive apps. In any case we have to create abstractions to make web page dynamic, so we use plain objects with the full power of JavaScript to create DOM tree in the fastest way.\n\n#### Why not to stick with CSS preprocessors like Stylus or Sass?\n\nYou simply don't need to use different CSS-like syntax with Swayer. JavaScript is more powerful and standardized language than any style preprocessor. Moreover, Swayer provides extended standard style declaration for convenience and brings selector abstraction, so you can just share or toggle styles as a simple JavaScript object. Swayer will distribute all your CSS rules across the app avoiding duplication and providing encapsulation.\n\n**Important: do not assume HTML or CSS to be legacy languages!**\u003cbr\u003e Swayer compiles component schemas down to the pure HTML/CSS on server side or directly to DOM/CSSOM in browsers while **making it consistent with your JavaScript logic**.\n\n## Features\n\n- #### General\n  - Tiny runtime\n  - Pure modern JavaScript everywhere\n  - Focus on performance and fast development\n  - MVVM design pattern\n  - ES6 modules as a single module system\n  - Fast asynchronous rendering\n  - No 3rd party dependencies in browser runtime\n  - Works in modern browsers and Node.js\n  - Server side rendering with hydration\n  - Environment modules\n\n- #### Components\n  - Declarative schema based components\n  - Atomicity and laziness by default\n  - Reactive state models\n  - Data binding\n  - Element reflection\n  - Context injection\n  - Namespaces\n  - Routing\n  - Lifecycle hooks\n- #### Styling\n  - Computed styles\n  - Sharable style rules\n  - CSS selector abstraction\n  - Extended property syntax\n  - Animations\n- #### Communication\n  - Events: bottom-up system or custom events\n  - Channels: scoped cross-component messages\n\n- #### Development tools\n  - Command Line Interface (CLI)\n    - create\n    - build\n    - render\n    - serve\n  - Typescript via JsDoc comments\n  - ESLint\n\n## Code examples\n\nSimple component describing a paragraph with text:\n\n```js\n/** @type {Schema} */\nexport default {\n  tag: 'p',\n  text: 'Hello World!',\n};\n```\n\nAdvanced component describing how text is being reflected in a paragraph while typing in a text input:\n\n```js\n/** @implements {ITextModel} */\nclass TextModel {\n  #defaultText = 'I am truly reactive!';\n  state = {\n    text: this.#defaultText,\n  };\n\n  update(text) {\n    this.state.text = text || this.#defaultText;\n  }\n}\n\n/** @type {Styles} */\nconst inputStyles = {\n  padding: '8px 14px',\n  fontSize: '14px',\n  borderRadius: '5px',\n  outline: 'none',\n  border: 'none',\n  boxShadow: '0 0 5px 0 #bababa',\n  transition: 'box-shadow 0.1s ease',\n  compute: (state) =\u003e ({\n    boxShadow: `0 0 ${state.text.length}px 0 #bababa`,\n  }),\n};\n\n/** @type {Schema\u003cITextModel\u003e} */\nexport default {\n  tag: 'div',\n  styles: { marginTop: '40px' },\n  model: new TextModel(),\n  children: [\n    {\n      tag: 'input',\n      styles: inputStyles,\n      attrs: {\n        type: 'text',\n        placeholder: 'Type something here...',\n      },\n      events: {\n        input() {\n          const value = this.props.value;\n          this.model.update(value);\n        },\n      },\n    },\n    {\n      tag: 'p',\n      styles: { fontWeight: 600 },\n      text: (state) =\u003e state.text,\n    },\n  ],\n};\n```\n\n## CLI usage\n\nAlmost all commands and options have short aliases, so use `npx swr --help` to see available commands and `npx swr [command] --help` to see specific command options. Look up the following sections to learn the most useful CLI cases. If you don't want to call `npx` all the time, you have to install the CLI globally: `npm i -g swayer`.\n\n`swr` - the name of CLI, shortened version of swayer word.\n\n**Serve app with HTTP server:**\n\n`npx swr serve` - serves the app in default client side rendering mode. In this case server renders only the valid HTML page boilerplate (html, head, title, body) and the body content is rendered in a browser. This command is typically used for development.\n\n`npx swr serve --mode ssr` - serves the app in server side rendering mode. In this case the whole page is rendered by the server including page body content, routing tree and ready hooks, which are awaited if async. A browser performs the invisible hydration to bind all schemas to HTML and make it dynamic.\n\n`npx swr serve someDir` - serves the project directory containing `main.js`.\n\nSee `npx swr serve --help` for more command options.\n\n**Build project:**\n\n`npx swr build` - prepares public build. By default, the application files will be placed in the default `dist` directory.\n\n`npx swr build --production` - prepares public build with additional source optimizations.\n\n`npx swr build --output outDir` - builds the project into a specific output folder.\n\nSee `npx swr build --help` for more command options.\n\n**Prerender HTML for multi page app:**\n\n`npx swr render main.js --output index.html` - consumes a schema component and outputs HTML. For now only root schema in `main.js` is available for this command.\n\n`npx swr render main.js -o index.html --route /page/one` - renders different HTML pages based on the `route` option. This is the way how the multipage app can be created: just change `--output` and `--route` options for each page.\n\nNote 1: `render` command also has `--mode` option, so consider it as the CLI version of the Swayer HTTP server functionality for rendering components.\n\nNote 2: rendered HTML pages are usually made in a build output directory (`dist` by default), so that any web server like Nginx can serve this artifact. Keep in mind - `build` command does not render any component by default.\n\nSee `npx swr render --help` for more command options.\n\n**Create SPA:**\n\n`npx swr spa` - creates a single `index.html` page to be used as an entry point for a single page application. This page should be served on any requested route and the engine router will do the rest.\n\nSee `npx swr spa --help` for more command options.\n\n## Swayer documentation\n\n### Table of contents\n\n1. [Terminology](#1-terminology)\n2. [Introduction](#2-introduction)\n3. [Swayer component system](#3-swayer-component-system)\n4. [The schema concept](#4-the-schema-concept)\n   - [Tag](#tag)\n   - [Text](#text)\n   - [Children](#children)\n   - [Attrs](#attrs)\n   - [Props](#props)\n   - [Classes](#classes)\n   - [Events](#events)\n   - [Channels](#channels)\n   - [Hooks](#hooks)\n5. [Schema Context API](#5-schema-context-api)\n6. [Reactivity](#6-reactivity)\n   - [Model](#model)\n   - [How Swayer reactivity reduces your work](#how-swayer-reactivity-reduces-your-work)\n7. [Styling](#7-styling)\n   - [Simple styles](#simple-styles)\n   - [Pseudo classes and elements](#pseudo-classes-and-elements)\n   - [Functional pseudo-classes](#functional-pseudo-classes)\n   - [Animations](#animations)\n   - [Computed styles](#computed-styles)\n   - [Inline styles](#inline-styles)\n8. [Routing](#8-routing)\n   - [Route types example](#route-types-example)\n   - [Router](#router)\n\n### 1. Terminology\n\n- **CLI** (Command Line Interface) - a JavaScript tool used to help developers create, build, render and serve components.\n- **CSR** (Client Side Rendering) - a rendering mode used to run engine in web browser and render dynamic application from schemas.\n- **Channel** - a pub/sub entity, that provides a name for scoped data emission and subscription based on event emitter.\n- **Component** - a schema, which is exported by default from module and groups other related schemas.\n- **Context API** - a set of properties and methods provided by engine to help developer use its features.\n- **Context** - an instance of schema created by engine during the rendering.\n- **Element** - a schema describing a DOM element.\n- **Engine** - Swayer UI engine.\n- **Environment** - a module defining variables for app configuration.\n- **Event** - an object, which flows from children to parents using native bubbling DOM events.\n- **Hash** - a hash string calculated from schema properties for self identifying.\n- **Hook** - an element lifecycle handler.\n- **Hydration** - a process of DOM node binding restoration using schema hash.\n- **Intercomponent communication** - a way of organizing data flow between different elements or components.\n- **Model** - an object describing reactive data state and its changes.\n- **Module** - an EcmaScript native module (ES6 module)\n- **Namespace** - a name, which encapsulates a part of path and is resolved by loader.\n- **Reaction** - a pure function, which binds model state to automate component reflection.\n- **Reactivity** - an ability of an object to update its properties on data change.\n- **Reflection** - a technique of metaprogramming, which enables instant data updates of underlying DOM, when schema properties change.\n- **Route** - an object that configures how component is being routed basing on some pattern.\n- **Router** - a routing controller, which is responsible for route management and navigation.\n- **Routing** - a mechanism, which maps routes to corresponding schemas relying on url path.\n- **SPA** (Single Page Application) - a rendering mode used to run engine in browser using a single predefined HTML page.\n- **SSR** (Server Side Rendering) - a rendering mode used to run engine with Node.js and render HTML from schemas.\n- **Schema child** - schema value, array of schema values or reaction.\n- **Schema children** - an array of schema values or reactions describing underlying structure.\n- **Component reference** - an object describing path and input for lazy loaded component.\n- **Schema value** - a value, which can be a regular schema, component reference or any primitive value including nullish ones.\n- **Schema** - an object describing element properties, structure and behavior.\n- **State** - an object containing mutable reactive data.\n- **Styles** - a schema property extending native CSSStyleDeclaration interface. Enables styling by abstracting CSS selectors.\n\n### 2. Introduction\n\nSwayer application is not trivial, because you don't have access neither to bare HTML, nor CSS. Instead, you should use pure JavaScript and you should never use DOM directly as it will break your app. It can be considered as a huge limitation, but it hides a lot of repetitive complexity, what reduces your work and improves overall development experience.\n\nTypical Swayer application has a defined file structure:\n\n- **main.js** - entry point of application, exports the root schema\n- **env.js** - current environment file, which is being substituted during the build\n- **app** - application source files\n- **assets** - images, icons, fonts and other static staff\n- **dist** - application build default directory (created by `swr build`)\n- **node_modules** - installed npm packages\n- **environments** - environment configurations (optional)\n- **types** - static typing files for better dev experience (optional)\n- project configuration files like package.json, .eslint.json, tsconfig.json, etc.\n\nThe minimal valid app must have a root schema in main.js. Consider this example:\n\n```js\n// exported root schema\nexport default {\n  children: [\n    // html element is the only one child possible in root schema\n    {\n      tag: 'html',\n      attrs: { lang: 'en' },\n      // html always consists of two mandatory children: head and body\n      children: [\n        {\n          tag: 'head',\n          children: [\n            // title is also mandatory for valid web pages\n            { tag: 'title', text: 'Page title' },\n          ],\n        },\n        // app content is always placed in the body\n        { tag: 'body', text: 'Simple text content' },\n      ],\n    },\n  ],\n}\n```\n\nThe initial project with basic structure can be created with\n`swr create mySwayerProject`, so that developer can concentrate on the application.\n\nThen there are several options to run a Swayer application:\n\n1. Use embedded http server with `swr serve` or `swr serve --mode=ssr` to run engine in SSR mode.\n2. Build SPA app with `swr build \u0026\u0026 swr spa` and serve index.html as a single page with any web server like Nginx.\n3. To build multipage app use `swr build`, then render an HTML page for each route with `swr render main.js --output myPage.html --route /` and serve these pages with any web server.\n4. More advanced option is to leverage engine server platform to render schemas in Node.js:\n\n```js\n// path to root schema module\nconst entryPath = 'main.js';\n// optional input data for root schema\nconst input = { data: 'Any data' };\n// optional path which is used for component routing\nconst routingPath = '/';\nconst platform = new ServerPlatform(options);\nconst content = await platform.render(entryPath, input, routingPath);\n\n// for more details see implementation: ./cli/httpServer.js\n```\n\n### 3. Swayer component system\n\nBasically, schema is a template from which engine instantiates contexts representing *N-ary tree data structure* and are traversed with *Depth first preorder tree traversal algorithm*. With some performance optimizations this approach delivers fast asynchronous rendering for best user experience. The size of a component can vary from single `p` or `span` element or even a piece of text to the whole application, which is represented by the root schema.\n\nTypical element can be described with such schema:\n\n```js\nconst paragraph = {\n  tag: 'p',\n  text: 'This is a paragraph',\n}\n```\n\nBut when `default export` is present, it becomes a component module:\n\n```js\nexport default {\n  tag: 'p',\n  text: 'This is a text component',\n};\n```\n\nIn case if a fragment is needed, schema array can be created as a component:\n\n```js\nexport default [\n  {\n    tag: 'p',\n    text: 'This is the first text component',\n  },\n  {\n    tag: 'p',\n    text: 'This is the second text component',\n  },\n];\n```\n\n**`export default` is mandatory for creating components**. Bare `export` won't work as the engine relies on default export.\n\nAs the application grows it becomes hard to manage all schemas in a single file. To address this issue Swayer uses **ES6 standard modules** to separate components and load them on demand. It's possible to lazily load such components with component reference:\n\n```js\n{\n  path: 'path/to/component',\n  input: { data: 'actually any optional data' },\n}\n```\n\nBy default, path is absolute and starts from site root. Relative paths are not available, because engine loader cannot know about component's base url. However, this path can be namespaced.\n\nSchemas can have not only the document structure, but also extensible features. One of them is **namespaces**, which is used for module path mapping. Conventionally namespaced paths start from `@` symbol and map to path from site root. Consider the following example:\n\n```js\n{\n  namespaces: {\n    '@foo': 'app/path/to/foo/folder',\n    '@bar': 'app/path/to/bar/folder',\n  },\n}\n```\n\nBy declaring namespaces developer gives an information about where engine can find modules to perform dynamic import under the hood or to send a message to distant component. Namespaces are scoped to the component it belongs to and all child components.\n\nConsider a `app/features/text` folder with file containing a text component - `text.component.js`:\n\n```js\nexport default {\n  tag: 'p',\n  text: 'This is a text component',\n};\n```\n\nNow we can create namespace and reference the text component:\n\n```js\n{\n  tag: 'div',\n  namespaces: {\n    '@text': 'app/features/text',\n  },\n  children: [\n    { path: '@text/text.component' }\n  ],\n}\n```\n\nAll modules are js files, so the extension can be skipped when using component reference.\n\nComponents can consume input if exported as a function, which is called **schema factory**. Consider the example, where title is passed to schema factory like an argument:\n\n```js\n{\n  path: '@path/to/title.component',\n  input: { title: 'My awesome title' },\n}\n```\n\n```js\nexport default ({ title }) =\u003e ({\n  tag: 'h1',\n  text: title,\n});\n```\n\nFactories allow developers to make their schemas fully dynamic parameterizing everything inside.\n\n### 4. The schema concept\n\nUnlike HTML-like template, schema is written in pure JavaScript, so we can use programming capabilities to make it really powerful. Schemas can have different types depending on its usage and can represent not only elements, but also bare text and some configurations like namespaces or routes. But first, let's go through the basic syntax of the **element schema**:\n\n#### Tag\n\nTag is obviously a name of HTML element tag. Used by engine to create a corresponding DOM element. Consider the simplest element schema:\n\n```js\n{\n  tag: 'div',\n}\n```\nThe piece of code above is rendered to `\u003cdiv\u003e\u003c/div\u003e` HTML element.\n\n#### Text\n\nText property corresponds to the element's text node. Any primitive value is valid as well as a reaction.\n\nThe following schema is rendered to `\u003cbutton\u003eClick me\u003c/button\u003e`:\n\n```js\n{\n  tag: 'button',\n  text: 'Click me',\n}\n```\n\nButton text is automatically updated, when the `state.text` is changed:\n\n```js\n{\n  tag: 'button',\n  // reaction\n  text: (state) =\u003e state.text,\n}\n```\n\n#### Children\n\nChildren include schemas, that belong to particular parent schema. Such approach is dictated by the tree-like nature of any web document. This array can hold any primitive values, schemas, component references or reactions:\n\n```js\n{\n  tag: 'div',\n  children: [\n    // simple element schema\n    { tag: 'span', text: 'Hello' },\n    // a bare text node\n    ' - ',\n    // simple element schema\n    { tag: 'span', text: 'world' },\n  ],\n}\n```\n\n```js\n{\n  tag: 'div',\n  children: [\n    // component reference\n    { path: '@path/to/some.component' },\n    // component reference with input data\n    {\n      path: '@path/to/title.component',\n      input: { title: 'A simple title' },\n    },\n  ],\n}\n```\n\n```js\n{\n  tag: 'div',\n  // children state reaction\n  children: (state) =\u003e state.items.map((item) =\u003e ({\n    tag: 'p',\n    text: item.text,\n  })),\n}\n```\n\n```js\n{\n  tag: 'div',\n  children: [\n    // child state reaction\n    (state) =\u003e ({ tag: 'p', text: state.text }),\n  ],\n}\n```\n\n#### Attrs\n\nAttributes object corresponds to a set of element's attributes:\n\n```js\n{\n  tag: 'input',\n  attrs: {\n    name: 'age',\n    type: 'text',\n  },\n}\n```\n\nSuch element is rendered to `\u003cinput name=\"age\" type=\"text\"\u003e`.\n\nReactions are also applicable to each attribute or the whole object:\n\n```js\n{\n  tag: 'input',\n  attrs: {\n    // attribute reaction\n    name: (state) =\u003e state.name,\n    type: 'text',\n  },\n}\n```\n\n```js\n{\n  tag: 'input',\n  // all attributes reaction\n  attrs: (state) =\u003e ({\n    name: state.name,\n    type: state.type,\n  }),\n}\n```\n\n#### Props\n\nProperties object corresponds to a set of element's properties. In the following example the value property is set to the input:\n\n```js\n{\n  tag: 'input',\n  props: {\n    value: 'Initial input value',\n  },\n}\n```\n\nUnlike attributes, properties belong directly to the DOM element object, so cannot be visualized in the markup.\n\nReactions are possible as well:\n\n```js\n{\n  tag: 'input',\n  props: {\n    // value property reaction\n    value: (state) =\u003e state.value,\n  },\n}\n```\n\n```js\n{\n  tag: 'input',\n  // all properties reaction\n  props: (state) =\u003e ({\n    value: state.value,\n  }),\n}\n```\n\n#### Classes\n\nClasses is a convenient alias for `attrs.class`, where developers can manage their custom css classes. Usually it becomes helpful, when using external css libraries like Bootstrap or font icons like FontAwesome. The valid value of this property is a string with space separated classes or an array of string classes:\n\n```js\n{\n  tag: 'i',\n  classes: 'fa fa-solid',\n}\n```\n\n```js\n{\n  tag: 'i',\n  classes: ['fa', 'fa-solid'],\n}\n```\n\nReaction can be also applied here:\n\n```js\n{\n  tag: 'i',\n  // reaction\n  classes: (state) =\u003e state.classes,\n}\n```\n\n#### Events\n\nEvents object is used to listen to system or custom DOM events. There is a native event mechanism used under the hood, so it's good to leverage **event delegation** for bubbling events. General usage is reacting for user actions. Another case is child-to-parent communication. Consider the following example:\n\n```js\n{\n  tag: 'input',\n  events: {\n    // event name matches any system events like click, mouseover, etc\n    input(event) {\n      // `this` instance is a reference to the Context API\n    },\n  },\n}\n```\n\nUsing the Context API developer can create custom events. For example:\n\n```js\nconst listItem = {\n  tag: 'li',\n  events: {\n    click() {\n      const data = { prop: 'some data' };\n      this.emitEvent('dataCreated', data);\n    },\n  },\n};\n```\n\nThen listen to custom event it in the upper elements:\n\n```js\n{\n  tag: 'ul',\n  events: {\n    // event name matches emitted custom event name\n    // get a detail as data from CustomEvent instance\n    dataCreated({ detail: data }) {\n      // do something with data here\n    },\n  },\n  children: [listItem],\n}\n```\n\n#### Channels\n\nThis feature implements **pub/sub** communication pattern and is used for **intercomponent messaging** providing a low level of code coupling. The implementation leverages **EventEmitter** under the hood to manage subscriptions. This is a powerful way of creating data flow between components whenever they are located in the project.\n\u003cbr\u003e\u003cbr\u003e\nTo prevent channel name conflicts, what is highly possible in big apps, a sender has to provide a **scope** of subscribers, so that only those components receive emitted messages. Scope accepts a folder or file path or an array of such paths. Paths can be absolute to the site root or namespaced like path in component reference. By default, messages are delivered to subscribers in the component boundaries if no scope specified.\n\u003cbr\u003e\u003cbr\u003e\nAnother crucial option of a channel message is a **select** function, which can help to deliver message only to those element contexts, which are selected by this function. Select function predicate must return boolean value to satisfy or filter out consumer contexts.\n\nConsider the following example using scoped channel:\n\n```js\n// message consumer\n{\n  tag: 'footer',\n  // object holding all message subscribers\n  channels: {\n    // create a 'headerMessage' topic subscriber\n    headerMessage(data) {\n      // handle passed data\n    }\n  },\n}\n```\n\n```js\n// message provider\n{\n  tag: 'header',\n  events: {\n    // send message to footer component on header click\n    click() {\n      const data = { prop: 'header data' };\n      const options = { scope: '@app/path/to/footer' };\n      this.emitMessage('headerMessage', data, options);\n    },\n  },\n}\n```\n\nUsing select option is helpful if we have multiple instances of the same schema on the page. Consider this example:\n\n```js\n// message consumer\nconst createListItem = (id) =\u003e ({\n  tag: 'li',\n  attrs: { id },\n  // object holding all message subscribers\n  channels: {\n    // create a 'changeItemMessage' topic subscriber\n    changeItemMessage(data) {\n      // handle passed data\n    }\n  },\n});\n```\n\n```js\n// message provider\n{\n  tag: 'ul',\n  events: {\n    // send message to selected li's on click\n    click() {\n      const data = { prop: 'message for concrete item' };\n      // select only second list item as a message consumer by id\n      const select = (ctx) =\u003e ctx.attrs === 'second-li';\n      this.emitMessage('changeItemMessage', data, { select });\n    },\n  },\n  // create multiple instances of the same schema\n  children: [\n    createListItem('first-li'),\n    createListItem('second-li'),\n    createListItem('third-li'),\n  ],\n}\n```\n\nNote: consider channels as a synchronous in-memory message broker with scope and select options as restrictions not to end up with lots of messy messages. Subscriptions are self-destroyable to prevent memory leaks.\n\n#### Hooks\n\nThese methods are typically used to run code at some point of element context lifecycle. For example, it's possible to initialize some data when the application is ready or when the context is destroyed to perform some cleaning actions. For now these hooks are available:\n\n```js\n{\n  tag: 'div',\n  hooks: {\n    // application is fully rendered and it's safe to perform changes\n    ready() {\n      // `this` is a reference to context API here\n    },\n    // the context of this schema is near to be destroyed, so it's\n    // the best place to clean up it\n    destroy() {\n      // `this` is a reference to context API here\n    },\n  },\n}\n```\n\n### 5. Schema Context API\n\nIn terms of Swayer engine a **context** is an object, which is created in runtime for each real DOM node. The best way to explain it is to think about the analogue: schema relates to its context like a class relates to its instance. This means, that we can create a lot of contexts from a single schema. While processing schemas, the engine creates contexts with predefined properties and methods providing an API to access data and the engine functionality. It's done using the method binding, so **it's very crucial to create methods, not arrow functions as they won't accept the context**.\n\nSome public context properties hold the current values provided by the schema like tag, text, attrs, etc. Some can reflect on the real node on change: text, attrs, props, classes, events; while others cannot be changed at all: tag, channels, hooks. Children property is fully internal and not visible in the context API, because it represents the DOM structure, which should not be modified imperatively. Instead, developers can only use reactions to mutate children in declarative way.\n\nAnother part of context API provides public engine functionality:\n\n- `moduleUrl: string` - get the full url of the component module. Helpful for locating the component.\n\u003cbr\u003e\u003cbr\u003e\n- `router: Router` - the object helping with the navigation. E.g. call `this.router.go(path)` to navigate to another route.\n\u003cbr\u003e\u003cbr\u003e\n- `emitEvent(name: string, data?: any): boolean` - emits a synthetic DOM event bubbling up through the component hierarchy, see [Events](#events) section for more details. Returns the result of native `dispatchEvent(event: Event): boolean`\n\u003cbr\u003e\u003cbr\u003e\n- `emitMessage(name: string, data?: any, options?: ChannelOptions): void` - emits a data message to the channel by name. See [Channels](#channels) section for more details. Returns void.\n\u003cbr\u003e\u003cbr\u003e\n- `click(): void` - native click method.\n\u003cbr\u003e\u003cbr\u003e\n- `focus(): void` - native focus method.\n\u003cbr\u003e\u003cbr\u003e\n- `blur(): void` - native blur method.\n\nSee [types/index.d.ts](https://github.com/rohiievych/swayer/blob/main/types/index.d.ts) for detailed typing information. This API will be extended in the future.\n\n### 6. Reactivity\n\nProbably the most powerful feature of the Swayer engine. Reactivity is the ability of an object to react on some changes. Reactivity reduces a lot of imperative code by defining **reactions** - pure arrow functions, that are invoked by the engine providing state binding to the schema context.\n\nThe basic syntax is `(state) =\u003e schema value`, where state is the special object containing reactive user data and schema value is the recalculated schema. Thanks to JavaScript Proxy and metaprogramming techniques, the engine reruns this function everytime the properties of the state, used in this function, are changed. Reactive approach allows us to concentrate on data removing all the trivial work, that is hidden in the engine machinery.\n\nReactions have some caveats to keep in mind:\n\n- **Reactions are synchronous, don't make them async**. This is a JavaScript Proxy limitation.\n- **Do not set state properties inside a reaction as will throw \"Maximum call stack size exceeded\" error**.\nThis is like the recursive function without exit condition.\n\nTo make things clear, consider the reactivity flow under the hood: `update state -\u003e call reaction(state) -\u003e reflect schema property -\u003e update DOM/CSSOM`.\n\nIf the state is updated inside the reaction, we get: `update state -\u003e reaction(state) -\u003e update state -\u003e reaction(state) ...` - endless recursion.\n\n#### Model\n\nModel is a schema property object, which is responsible for holding state and its mutations. Thus, `state` is the mandatory property in every model. It is also the part of [Context API](#5-schema-context-api) and can be used inside events, channels or hooks. Defining the model as a separate class makes schemas concise. Therefore, when developer create a model, he can fully focus on the state and how it is going to be mutated providing some model methods. In terms of MVVM, view model is a schema and the model is the object with state described above.\n\nModel have some caveats to keep in mind:\n\n- The model scope is restricted to the component if it's defined in the root element of that component, what means it's shared among all it's elements, excluding those, which referenced as child components. This is done to prevent state leakage through the whole app, but making it useful across the component.\n- If a model is defined for the concrete element, it takes a precedence over the component's one.\n- If a model is defined in a static schema, the related state will stay the same for all instantiated contexts. To make it unique for each context, it must be recreated with schema factory.\n\n#### How Swayer reactivity reduces your work\n\nSee the following component example showing how reactivity works:\n\n```js\n{\n  tag: 'div',\n  // define a model\n  model: {\n    // define a state with some data inside\n    state: {\n      // this is a reactive property\n      text: 'Initial text',\n    },\n    // a method to mutate a state\n    update(newText) {\n      this.state.text = newText;\n    },\n  },\n  children: [\n    {\n      tag: 'input',\n      attrs: {\n        type: 'text',\n        placeholder: 'Type here...',\n      },\n      events: {\n        input() {\n          // update the model on text input\n          // this will trigger the reaction in p element\n          const text = this.props.value;\n          this.model.update(text);\n        },\n      },\n    },\n    {\n      tag: 'p',\n      // reaction with state recalculation, state is the model.state\n      // described above in the root component\n      text: (state) =\u003e 'Hello ' + state.text,\n    }\n  ],\n}\n```\n\nIn case of vanilla JS, we would implement the same functionality in this way:\n\n```html\n\u003c!-- Initial HTML markup --\u003e\n\u003cdiv\u003e\n  \u003cinput type=\"text\", placeholder=\"Type here...\"\u003e\n  \u003cp\u003eInitial text\u003c/p\u003e\n\u003c/div\u003e\n```\n\n```js\n// Get paragraph element imperatively and create text updater\nconst p = document.querySelector('p');\nconst updatePara = (text) =\u003e p.textContent = 'Hello ' + text;\n\n// Get input element imperatively and bind updater to text input\nconst input = document.querySelector('input');\ninput.addEventListener('input', (event) =\u003e updateText(event.target.value));\n```\n\nThe difference in development time is not very big here. But what if we want to modify this feature and add another text container with same text updates? In the schema based approach, we need to add a new child near the paragraph:\n\n```js\n{\n  tag: 'span',\n  text: (state) =\u003e 'Hello ' + state.text,\n}\n```\n\nDo the same with vanilla:\n\n```html\n\u003c!-- Update markup with a new span --\u003e\n\u003cspan\u003eInitial text\u003c/span\u003e\n```\n\n```js\n// Get span element imperatively and create text updater\nconst span = document.querySelector('span');\nconst updateSpan = (text) =\u003e span.textContent = 'Hello ' + text;\n\n// Get input element imperatively and bind updater to text input\nconst input = document.querySelector('input');\ninput.addEventListener('input', (event) =\u003e updateSpan(event.target.value));\n```\n\nThen we have to perform refactoring to reduce the code, but the guy using schema is already implementing the next feature... So we have just done in three times more work! It could be done better using React, but try to apply such change just in two lines of code - developer still need to write another piece of JSX markup and a bit of scripts. Swayer engine will do it automatically, allowing developer to concentrate on features. So what does the engine here - it guarantees the linear time of development, while keeping the app performant, readable and reusable.\n\n### 7. Styling\n\nAll styles are written in schemas using JavaScript. This means that developer don't need to use additional preprocessors with random syntax. Moreover, no need to write even CSS selectors thankfully to **CSS selector abstraction**. To style the element developer just need to define the `styles` object in element schema, which can be also shared among other element schemas as a plain object. The engine will effectively resolve these styles into the pieces of pure CSS directly in markup or CSSOM. So the page will contain only those styles, that used for elements on current page and not more.\n\nThe engine extends styling syntax by adding intuitive properties like **hover** as it would be another set of CSS properties. Pseudo-classes, pseudo-elements and animations are implemented too.\n\nLet's go through the styling capabilities:\n\n#### Simple styles\n\n```js\n{\n  tag: 'p',\n  styles: {\n    // add some CSS properties like in the CSS rule,\n    // but in camelCase\n    fontSize: '14px',\n    color: 'red',\n  },\n}\n```\n\n#### Pseudo classes and elements\n\n```js\n{\n  tag: 'p',\n  styles: {\n    transition: 'backgroundColor 0.2s ease',\n    // make this component blue on hover\n    hover: {\n      backgroundColor: 'blue',\n    },\n    // make the first-of-type text red\n    first: {\n      color: 'red',\n    },\n  },\n}\n```\n\n```js\n{\n  tag: 'p',\n  styles: {\n    color: 'red',\n    // make the first-of-type blue on hover\n    first: {\n      transition: 'backgroundColor 0.2s ease',\n      hover: {\n        backgroundColor: 'blue',\n      },\n    },\n  },\n}\n```\n\n```js\n{\n  tag: 'p',\n  styles: {\n    position: 'relative',\n    // add before pseudo-element\n    before: {\n      content: `''`,\n      position: 'absolute',\n      right: '0',\n    },\n  },\n}\n```\n\n#### Functional pseudo-classes\n\n```js\n{\n  tag: 'p',\n  styles: {\n    // apply style rule equivalently to nth-of-type(2n)\n    nth: {\n      arg: '2n',\n      rule: {\n        borderBottom: '1px solid red',\n        color: 'red',\n      },\n    },\n  },\n}\n```\n\n#### Animations\n\n```js\n{\n  tag: 'div',\n  styles: {\n    // create multiple animations and apply them to element\n    animations: [\n      {\n        // CSS animation name\n        name: 'fadeIn',\n        // CSS animation properties\n        props: 'linear 3s',\n        // CSS animation keyframes\n        keyframes: {\n          'from': {\n            opacity: 0,\n          },\n          '50%': {\n            opacity: 0.5,\n          },\n          'to': {\n            opacity: 1,\n          },\n        },\n      },\n      {\n        name: 'fadeOut',\n        props: 'linear 3s',\n        keyframes: {\n          from: {\n            opacity: 1,\n          },\n          to: {\n            opacity: 0,\n          },\n        },\n      },\n    ],\n  },\n}\n```\n\n```js\n{\n  tag: 'p',\n  styles: {\n    // apply existing animations to element\n    animations: [\n      { name: 'fadeIn' },\n      { name: 'fadeOut', props: 'ease-out 2s' },\n    ],\n  },\n}\n```\n\n#### Computed styles\n\nStyles can be written with reactions too. This is very helpful as developer can bind a model state to the styles, so there is no more need to mess with class toggling. Simply create a reaction and return styles calculated on the state.\n\nConsider the following examples:\n\n```js\n{\n  tag: 'p',\n  model: {\n    state: {\n      isValid: true,\n    },\n  },\n  // all styles are changed on `state.isValid` value change\n  styles: (state) =\u003e state.isValid\n    ? { color: 'black' }\n    : { color: 'red' },\n}\n```\n\n```js\n{\n  tag: 'p',\n  model: {\n    state: {\n      isValid: true,\n    },\n  },\n  styles: {\n    // these styles won't be affected\n    fontSize: '14px',\n    fontWeight: 'bold',\n    // compute only the needed styles\n    compute: (state) =\u003e state.isValid\n      ? { color: 'black' }\n      : { color: 'red' },\n  },\n}\n```\n\n#### Inline styles\n\nAdditionally, it's possible to add inline styles in `attrs.style` as an object. Unlike the `styles` property, these styles inlined into the style attribute.\n\n```js\n{\n  tag: 'p',\n  attrs: {\n    style: {\n      // these props will be inlined\n      fontSize: '14px',\n      color: 'red',\n    },\n  },\n}\n```\n\nThe code above is rendered to: `\u003cp style=\"font-size: 14px; color: red;\"\u003e\u003c/p\u003e`\n\n\n### 8. Routing\n\nRouting is the mechanism used for navigation through different components basing on the url path. Routing is the crucial part of any Single Page Application as it performs navigation without full page reload. The engine provides a special schema, where the right route is matched and the corresponding schema is rendered. This is done by creating a tree of routers for each group of routes.\n\n#### Route types example\n\n```js\n{\n  tag: 'div',\n  children: [\n    {\n      // for each group of routes a child router is created\n      // child router holds the segment of the path\n      // excluding parent's segment\n      routes: [\n        // pattern is the string, that the url path is going to match\n        // in this case empty string means no path, it will be applied\n        // for '/'\n        {\n          pattern: '',\n          // the element schema to be rendered\n          schema: {\n            tag: 'p',\n            text: 'Paragraph element is routed!',\n          },\n        },\n        {\n          // matches /products\n          pattern: 'products',\n\n          // the component reference schema to be rendered\n          schema: {\n            path: '@app/path/to/products.component',\n            input: 'any data',\n          },\n        },\n        {\n          // pattern supports parameters with :param syntax\n          // matches /products/12345\n          pattern: 'products/:id',\n\n          // a function that allows route to be matched on some\n          // condition, useful to validate params\n          canMatch: (params) =\u003e params.id === '12345',\n\n          // schema can be represented as arrow function or async\n          // function that extracts params from path returing\n          // a schema to be rendered\n          schema: (params) =\u003e ({\n            tag: 'p',\n            text: `Product ${params.id} is routed!`,\n          }),\n        },\n        {\n          // array pattern makes a union of possible path segments\n          // consider this as || (or), matches /admin or /adm\n          pattern: ['admin', 'adm'],\n\n          // simple text schema to be rendered\n          schema: 'Here will be admin page!',\n        },\n        {\n          // any route pattern, useful when redirecting to 404 page\n          // if no route match found, this will be selected\n          // matches /any/path\n          pattern: '**',\n          schema: '404',\n        },\n      ],\n    }\n  ],\n}\n```\n\nThe routes in one group have a match, where the winner is the route with the most specific pattern, so **the order of routes matters**.\n\nHere is a small cheat sheet example for routes matching order:\n1. `''` (empty path segment)\n2. `:param` (parameter segment)\n3. `products` (static segment)\n4. `**` (any segment)\n\n#### Router\n\nThe Context API provides a `router` property. Currently, one method can be used:\n- Go to the relative path segment, applied to the nearest router: `this.router.go('path/segment')`\n- Go to the absolute path, applied to the root router: `this.router.go('/root/path')`\n\n`go` method updates the path or path segment and reloads corresponding router, which will match his routes against new path.\n\nExample:\n\n```js\n{\n  tag: 'button',\n  events: {\n    click() {\n      this.router.go('/root/path');\n    },\n  },\n}\n```\n\n## Compatibility\n\n- Chromium based browsers (v80+)\n- Firefox (v90+)\n- Safari (v14.1+)\n- Opera (v67+)\n- Node.js (v16+)\n\n## License \u0026 Contributors\n\nCopyright (c) 2023 Roman Ohiievych.\u003cbr\u003e\nSee GitHub for full [contributors list](https://github.com/rohiievych/swayer/graphs/contributors).\u003cbr\u003e\nSwayer framework is [MIT licensed](./LICENSE).\u003cbr\u003e\nOriginal author: \u0026lt;roman@swayer.dev\u0026gt;\u003cbr\u003e\n\n**Totally made in Ukraine** 🇺🇦\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frohiievych%2Fswayer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frohiievych%2Fswayer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frohiievych%2Fswayer/lists"}