From 439ed43aec9ece6948dc97559cd5c5a579e43a8b Mon Sep 17 00:00:00 2001
From: 4eb0da <4eb0da@yandex-team.com>
Date: Tue, 17 Oct 2023 15:22:33 +0300
Subject: [PATCH] Custom components support
---
.mapping.json | 23 +
.../divkit-examples/custom-container/.npmrc | 1 +
.../custom-container/README.md | 42 ++
.../custom-container/customElement.js | 9 +
.../custom-container/index.html | 86 +++
.../custom-container/package-lock.json | 22 +
.../custom-container/package.json | 14 +
.../web/divkit-examples/custom-simple/.npmrc | 1 +
.../divkit-examples/custom-simple/README.md | 94 +++
.../custom-simple/customElement.js | 56 ++
.../divkit-examples/custom-simple/index.html | 52 ++
.../custom-simple/package-lock.json | 22 +
.../custom-simple/package.json | 14 +
client/web/divkit-examples/custom-ssr/.npmrc | 1 +
.../web/divkit-examples/custom-ssr/README.md | 131 ++++
.../custom-ssr/customElement.js | 11 +
.../custom-ssr/package-lock.json | 666 ++++++++++++++++++
.../divkit-examples/custom-ssr/package.json | 15 +
.../web/divkit-examples/custom-ssr/server.js | 107 +++
.../divkit-examples/custom-ssr/template.html | 36 +
client/web/divkit/README.md | 16 +
client/web/divkit/src/client.ts | 27 +-
client/web/divkit/src/components.js | 3 +-
client/web/divkit/src/components/Root.svelte | 4 +-
.../src/components/custom/Custom.svelte | 98 +++
client/web/divkit/src/components/typeMap.ts | 5 +
client/web/divkit/src/context/root.ts | 2 +
client/web/divkit/src/dev.ts | 18 +
client/web/divkit/src/devCustomComponents.ts | 80 +++
client/web/divkit/src/server.ts | 14 +-
client/web/divkit/src/types/custom.d.ts | 9 +
client/web/divkit/typings/client-devtool.d.ts | 2 +
.../web/divkit/typings/client-hydratable.d.ts | 2 +
client/web/divkit/typings/client.d.ts | 2 +
client/web/divkit/typings/custom.d.ts | 5 +
client/web/divkit/typings/server.d.ts | 2 +
schema/div-custom.json | 4 -
schema/div.json | 6 +-
test_data/regression_test_data/index.json | 8 -
39 files changed, 1660 insertions(+), 50 deletions(-)
create mode 100644 client/web/divkit-examples/custom-container/.npmrc
create mode 100644 client/web/divkit-examples/custom-container/README.md
create mode 100644 client/web/divkit-examples/custom-container/customElement.js
create mode 100644 client/web/divkit-examples/custom-container/index.html
create mode 100644 client/web/divkit-examples/custom-container/package-lock.json
create mode 100644 client/web/divkit-examples/custom-container/package.json
create mode 100644 client/web/divkit-examples/custom-simple/.npmrc
create mode 100644 client/web/divkit-examples/custom-simple/README.md
create mode 100644 client/web/divkit-examples/custom-simple/customElement.js
create mode 100644 client/web/divkit-examples/custom-simple/index.html
create mode 100644 client/web/divkit-examples/custom-simple/package-lock.json
create mode 100644 client/web/divkit-examples/custom-simple/package.json
create mode 100644 client/web/divkit-examples/custom-ssr/.npmrc
create mode 100644 client/web/divkit-examples/custom-ssr/README.md
create mode 100644 client/web/divkit-examples/custom-ssr/customElement.js
create mode 100644 client/web/divkit-examples/custom-ssr/package-lock.json
create mode 100644 client/web/divkit-examples/custom-ssr/package.json
create mode 100644 client/web/divkit-examples/custom-ssr/server.js
create mode 100644 client/web/divkit-examples/custom-ssr/template.html
create mode 100644 client/web/divkit/src/components/custom/Custom.svelte
create mode 100644 client/web/divkit/src/devCustomComponents.ts
create mode 100644 client/web/divkit/src/types/custom.d.ts
create mode 100644 client/web/divkit/typings/custom.d.ts
diff --git a/.mapping.json b/.mapping.json
index ec09869fa..27bc8fe1d 100644
--- a/.mapping.json
+++ b/.mapping.json
@@ -11835,6 +11835,25 @@
"client/ios/codegen_DivKit.sh":"divkit/public/client/ios/codegen_DivKit.sh",
"client/ios/codegen_Tests.sh":"divkit/public/client/ios/codegen_Tests.sh",
"client/web/divkit-examples/README.md":"divkit/public/client/web/divkit-examples/README.md",
+ "client/web/divkit-examples/custom-container/.npmrc":"divkit/public/client/web/divkit-examples/custom-container/.npmrc",
+ "client/web/divkit-examples/custom-container/README.md":"divkit/public/client/web/divkit-examples/custom-container/README.md",
+ "client/web/divkit-examples/custom-container/customElement.js":"divkit/public/client/web/divkit-examples/custom-container/customElement.js",
+ "client/web/divkit-examples/custom-container/index.html":"divkit/public/client/web/divkit-examples/custom-container/index.html",
+ "client/web/divkit-examples/custom-container/package-lock.json":"divkit/public/client/web/divkit-examples/custom-container/package-lock.json",
+ "client/web/divkit-examples/custom-container/package.json":"divkit/public/client/web/divkit-examples/custom-container/package.json",
+ "client/web/divkit-examples/custom-simple/.npmrc":"divkit/public/client/web/divkit-examples/custom-simple/.npmrc",
+ "client/web/divkit-examples/custom-simple/README.md":"divkit/public/client/web/divkit-examples/custom-simple/README.md",
+ "client/web/divkit-examples/custom-simple/customElement.js":"divkit/public/client/web/divkit-examples/custom-simple/customElement.js",
+ "client/web/divkit-examples/custom-simple/index.html":"divkit/public/client/web/divkit-examples/custom-simple/index.html",
+ "client/web/divkit-examples/custom-simple/package-lock.json":"divkit/public/client/web/divkit-examples/custom-simple/package-lock.json",
+ "client/web/divkit-examples/custom-simple/package.json":"divkit/public/client/web/divkit-examples/custom-simple/package.json",
+ "client/web/divkit-examples/custom-ssr/.npmrc":"divkit/public/client/web/divkit-examples/custom-ssr/.npmrc",
+ "client/web/divkit-examples/custom-ssr/README.md":"divkit/public/client/web/divkit-examples/custom-ssr/README.md",
+ "client/web/divkit-examples/custom-ssr/customElement.js":"divkit/public/client/web/divkit-examples/custom-ssr/customElement.js",
+ "client/web/divkit-examples/custom-ssr/package-lock.json":"divkit/public/client/web/divkit-examples/custom-ssr/package-lock.json",
+ "client/web/divkit-examples/custom-ssr/package.json":"divkit/public/client/web/divkit-examples/custom-ssr/package.json",
+ "client/web/divkit-examples/custom-ssr/server.js":"divkit/public/client/web/divkit-examples/custom-ssr/server.js",
+ "client/web/divkit-examples/custom-ssr/template.html":"divkit/public/client/web/divkit-examples/custom-ssr/template.html",
"client/web/divkit-examples/divkit-react/.npmrc":"divkit/public/client/web/divkit-examples/divkit-react/.npmrc",
"client/web/divkit-examples/divkit-react/README.md":"divkit/public/client/web/divkit-examples/divkit-react/README.md",
"client/web/divkit-examples/divkit-react/index.html":"divkit/public/client/web/divkit-examples/divkit-react/index.html",
@@ -11925,6 +11944,7 @@
"client/web/divkit/src/components/container/Container.svelte":"divkit/public/client/web/divkit/src/components/container/Container.svelte",
"client/web/divkit/src/components/container/ContainerSeparator.module.css":"divkit/public/client/web/divkit/src/components/container/ContainerSeparator.module.css",
"client/web/divkit/src/components/container/ContainerSeparators.svelte":"divkit/public/client/web/divkit/src/components/container/ContainerSeparators.svelte",
+ "client/web/divkit/src/components/custom/Custom.svelte":"divkit/public/client/web/divkit/src/components/custom/Custom.svelte",
"client/web/divkit/src/components/gallery/Gallery.module.css":"divkit/public/client/web/divkit/src/components/gallery/Gallery.module.css",
"client/web/divkit/src/components/gallery/Gallery.svelte":"divkit/public/client/web/divkit/src/components/gallery/Gallery.svelte",
"client/web/divkit/src/components/grid/Grid.module.css":"divkit/public/client/web/divkit/src/components/grid/Grid.module.css",
@@ -11969,6 +11989,7 @@
"client/web/divkit/src/context/state.ts":"divkit/public/client/web/divkit/src/context/state.ts",
"client/web/divkit/src/dev.html":"divkit/public/client/web/divkit/src/dev.html",
"client/web/divkit/src/dev.ts":"divkit/public/client/web/divkit/src/dev.ts",
+ "client/web/divkit/src/devCustomComponents.ts":"divkit/public/client/web/divkit/src/devCustomComponents.ts",
"client/web/divkit/src/expressions/ast.d.ts":"divkit/public/client/web/divkit/src/expressions/ast.d.ts",
"client/web/divkit/src/expressions/bigint.ts":"divkit/public/client/web/divkit/src/expressions/bigint.ts",
"client/web/divkit/src/expressions/const.ts":"divkit/public/client/web/divkit/src/expressions/const.ts",
@@ -12003,6 +12024,7 @@
"client/web/divkit/src/types/base.d.ts":"divkit/public/client/web/divkit/src/types/base.d.ts",
"client/web/divkit/src/types/border.d.ts":"divkit/public/client/web/divkit/src/types/border.d.ts",
"client/web/divkit/src/types/container.d.ts":"divkit/public/client/web/divkit/src/types/container.d.ts",
+ "client/web/divkit/src/types/custom.d.ts":"divkit/public/client/web/divkit/src/types/custom.d.ts",
"client/web/divkit/src/types/drawable.d.ts":"divkit/public/client/web/divkit/src/types/drawable.d.ts",
"client/web/divkit/src/types/edgeInserts.d.ts":"divkit/public/client/web/divkit/src/types/edgeInserts.d.ts",
"client/web/divkit/src/types/filter.d.ts":"divkit/public/client/web/divkit/src/types/filter.d.ts",
@@ -14164,6 +14186,7 @@
"client/web/divkit/typings/client-hydratable.d.ts":"divkit/public/client/web/divkit/typings/client-hydratable.d.ts",
"client/web/divkit/typings/client.d.ts":"divkit/public/client/web/divkit/typings/client.d.ts",
"client/web/divkit/typings/common.d.ts":"divkit/public/client/web/divkit/typings/common.d.ts",
+ "client/web/divkit/typings/custom.d.ts":"divkit/public/client/web/divkit/typings/custom.d.ts",
"client/web/divkit/typings/extensions.d.ts":"divkit/public/client/web/divkit/typings/extensions.d.ts",
"client/web/divkit/typings/server.d.ts":"divkit/public/client/web/divkit/typings/server.d.ts",
"client/web/divkit/typings/variables.d.ts":"divkit/public/client/web/divkit/typings/variables.d.ts",
diff --git a/client/web/divkit-examples/custom-container/.npmrc b/client/web/divkit-examples/custom-container/.npmrc
new file mode 100644
index 000000000..cffe8cdef
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/.npmrc
@@ -0,0 +1 @@
+save-exact=true
diff --git a/client/web/divkit-examples/custom-container/README.md b/client/web/divkit-examples/custom-container/README.md
new file mode 100644
index 000000000..16e3233a4
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/README.md
@@ -0,0 +1,42 @@
+## Using DivKit components inside your Custom Component
+
+[General notes about custom components](../../divkit/README.md#customComponents)
+
+[Previous example](../custom-simple)
+
+Install the packages first:
+
+```
+npm ci
+```
+
+And then open `index.html` file with any http server to see the result (the browser may block loading of external modules into the file:// pages).
+
+### How can support a DivKitComponents inside my Custom Component?
+
+First of all, lets look at the markup:
+
+```js
+ // DivJson
+ "type": "custom",
+ "custom_type": "custom_container",
+ "items": [
+ ...
+ ]
+```
+
+These `items` are the DivKit markup and will be rendered by DivKit itself.
+
+Now we will provide a place (`slot`) for these `items`:
+
+```js
+ // JS code
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+```
+
+This `` can be located at any depth of your element, not just a direct child of ShadowRoot.
+
+### How can I render Custom DivKit components on the Server Side?
+
+See [next example](../custom-ssr).
diff --git a/client/web/divkit-examples/custom-container/customElement.js b/client/web/divkit-examples/custom-container/customElement.js
new file mode 100644
index 000000000..357f99cbb
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/customElement.js
@@ -0,0 +1,9 @@
+class CustomContainer extends HTMLElement {
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ }
+}
+customElements.define('custom-container', CustomContainer);
diff --git a/client/web/divkit-examples/custom-container/index.html b/client/web/divkit-examples/custom-container/index.html
new file mode 100644
index 000000000..7d2d1f9a0
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/index.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/divkit-examples/custom-container/package-lock.json b/client/web/divkit-examples/custom-container/package-lock.json
new file mode 100644
index 000000000..a1851a256
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/package-lock.json
@@ -0,0 +1,22 @@
+{
+ "name": "custom-simple",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "custom-simple",
+ "version": "1.0.0",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
+ }
+ },
+ "node_modules/@divkitframework/divkit": {
+ "version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
+ "resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
+ "integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
+ "dev": true
+ }
+ }
+}
diff --git a/client/web/divkit-examples/custom-container/package.json b/client/web/divkit-examples/custom-container/package.json
new file mode 100644
index 000000000..5bb3cdd9f
--- /dev/null
+++ b/client/web/divkit-examples/custom-container/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "custom-simple",
+ "private": true,
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {},
+ "keywords": [],
+ "author": "",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
+ }
+}
diff --git a/client/web/divkit-examples/custom-simple/.npmrc b/client/web/divkit-examples/custom-simple/.npmrc
new file mode 100644
index 000000000..cffe8cdef
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/.npmrc
@@ -0,0 +1 @@
+save-exact=true
diff --git a/client/web/divkit-examples/custom-simple/README.md b/client/web/divkit-examples/custom-simple/README.md
new file mode 100644
index 000000000..83be45693
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/README.md
@@ -0,0 +1,94 @@
+## Using custom components
+
+[General notes about custom components](../../divkit/README.md#customComponents)
+
+Install the packages first:
+
+```
+npm ci
+```
+
+And then open `index.html` file with any http server to see the result (the browser may block loading of external modules into the file:// pages).
+
+### What's the idea?
+
+In json markup you can provide a special component:
+
+```json
+// DivJson
+{
+ "type": "custom",
+ "custom_type": "custom_card"
+}
+```
+
+Without any help, DivKit will not know about this special component. This can be done using the `customComponents` map:
+
+```js
+// JS code
+render({
+ id: 'test',
+ target: document.querySelector('#root'),
+ customComponents: new Map([
+ ['custom_card', {
+ element: 'custom-card'
+ }]
+ ]),
+ json: {
+ ...
+ }
+});
+```
+
+Now we need to create our `custom-card` component. This can be done using the power of Custom Elements:
+
+```js
+// JS Code
+class CustomCard extends HTMLElement {
+ constructor() {
+ super();
+
+ // some logic
+ }
+}
+customElements.define('custom-card', CustomCard);
+```
+
+This code defines a custom element inside the browser. So, any `custom` component inside DivKit should be a Custom Element in terms of HTML.
+
+### How can I provide additional data for a component?
+
+`custom` components in DivKit can accept an additional field `custom_props`. This is an object, and all its properties are directly mapped as attributes of your Custom Element:
+
+```js
+ // DivJson
+ "custom_props": {
+ "start": 15
+ }
+
+ // ->
+
+ // HTML
+
+```
+
+Keep in mind that there are no integer attributes here, all additional properties will be rendered as regular html attributes.
+
+Next, your Custom Element should read these properties:
+
+```js
+ // JS code
+ connectedCallback() {
+ const start = this.getAttribute('start');
+ }
+```
+
+Keep in mind that these additional attributes will only be available in the `connectedCallback`, and not in the `constructor` of the Custom Element.
+
+### What about browser support?
+
+Custom elements have been supported in all modern browsers for a while. There are some nuances that will be covered in the following examples, but basic support for Custom Elements v1 will be a good start for your components.
+
+### How can I wrap other DivKit components with my Custom Component?
+
+See [next example](../custom-container).
diff --git a/client/web/divkit-examples/custom-simple/customElement.js b/client/web/divkit-examples/custom-simple/customElement.js
new file mode 100644
index 000000000..c109bf6ad
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/customElement.js
@@ -0,0 +1,56 @@
+class CustomCard extends HTMLElement {
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+
+ const style = document.createElement('style');
+
+ style.textContent = `
+ .component {
+ padding: 30px;
+ font-size: 20px;
+ background: linear-gradient(85deg, #fff, #068200, #1eea00, #FF0000, #fcfa1e, #ccd49b, #7af719);
+ }
+ `;
+
+ const div = this._text = document.createElement('div');
+ div.className = 'component';
+ div.textContent = '00:00';
+
+ shadow.appendChild(style);
+ shadow.appendChild(div);
+ }
+
+ _updateText() {
+ const minutes = Math.floor(this._counter / 60);
+ const seconds = this._counter % 60;
+ this._text.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
+ }
+
+ _tick() {
+ this._timeout = window.setTimeout(() => {
+ ++this._counter;
+
+ this._updateText();
+
+ this._tick();
+ }, 1000);
+ }
+
+ connectedCallback() {
+ const start = this.getAttribute('start');
+ this._counter = Number(start) || 0;
+ this._timeout = 0;
+ this._updateText();
+ this._tick();
+ }
+
+ disconnectedCallback() {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = 0;
+ }
+ }
+}
+customElements.define('custom-card', CustomCard);
diff --git a/client/web/divkit-examples/custom-simple/index.html b/client/web/divkit-examples/custom-simple/index.html
new file mode 100644
index 000000000..3325ed21a
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/web/divkit-examples/custom-simple/package-lock.json b/client/web/divkit-examples/custom-simple/package-lock.json
new file mode 100644
index 000000000..a1851a256
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/package-lock.json
@@ -0,0 +1,22 @@
+{
+ "name": "custom-simple",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "custom-simple",
+ "version": "1.0.0",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
+ }
+ },
+ "node_modules/@divkitframework/divkit": {
+ "version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
+ "resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
+ "integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
+ "dev": true
+ }
+ }
+}
diff --git a/client/web/divkit-examples/custom-simple/package.json b/client/web/divkit-examples/custom-simple/package.json
new file mode 100644
index 000000000..5bb3cdd9f
--- /dev/null
+++ b/client/web/divkit-examples/custom-simple/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "custom-simple",
+ "private": true,
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {},
+ "keywords": [],
+ "author": "",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb"
+ }
+}
diff --git a/client/web/divkit-examples/custom-ssr/.npmrc b/client/web/divkit-examples/custom-ssr/.npmrc
new file mode 100644
index 000000000..cffe8cdef
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/.npmrc
@@ -0,0 +1 @@
+save-exact=true
diff --git a/client/web/divkit-examples/custom-ssr/README.md b/client/web/divkit-examples/custom-ssr/README.md
new file mode 100644
index 000000000..8986ad09f
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/README.md
@@ -0,0 +1,131 @@
+## Using DivKit components inside your Custom Component
+
+[General notes about custom components](../../divkit/README.md#customComponents)
+
+[Previous example with a simple component](../custom-simple)
+
+[Previous example with container component](../custom-container)
+
+[General info about SSR in DivKit](../ssr)
+
+This example assumes that you are familiar with the ideas from the previous examples.
+
+Install the packages first:
+
+```
+npm ci
+```
+
+### Run sample
+
+```shell
+npm start
+```
+
+### How does it work without items?
+
+The server code contains a similar `customComponents` map and will produce something like this:
+
+```html
+ // HTML
+
+```
+
+On the client side, this markup will be hydrated. So, what about markup inside the Custom Element?
+
+Lets assume that our component has some markup, for example, `Hello custom container`:
+
+```html
+ // HTML
+
+
+ Hello custom container
+
+
+```
+
+Custom elements is defined on the client side, and their markup is created in the browser. This means that if we don't do anything, our glorious SSR (Server-side rendering) will produce incomplete markup, which will be changed after the Custom Element is defined. The component will work as expected, but the user may notice a layout shift and a markup change.
+
+Declarative shadow dom comes to the rescue! If our `customComponents` contain not only the tag name of the Custom Element, but also a template itself, DivKit will produce a `` for this component for us:
+
+```js
+ // JS code
+ {
+ element: 'custom-container',
+ template: 'Hello custom container'
+ }
+
+ // HTML
+
+
+ Hello custom container
+
+
+```
+
+This markup will be parsed in the browser directly into the shadow dom:
+
+```html
+
+ #shadow-root (open)
+ Hello custom container
+
+```
+
+In this case, the browser will be able to show the exact page even without running JS on the client side, which means that there will be no layout shift on our page and it will be persisted during the entire page load.
+
+Don't forget to change your custom element code:
+
+```js
+ // JS Code
+ if (this.shadowRoot) {
+ // Declarative shadow dom is supported and parsed by the browser
+ } else {
+ // Create a shadow root as usual
+ const shadow = this.attachShadow({ mode: 'open' });
+ // ...
+ }
+```
+
+### How it works with items?
+
+Literally the same. Don't forget to include `` in your template, and everything will be fine:
+
+```js
+ // JS code
+ {
+ element: 'custom-container',
+ template: 'Hello custom container'
+ }
+
+ // HTML Server response
+
+
+ Hello custom container
+
+
+
+
+
+
+
+ // In the browser parsed HTML
+
+ #shadow-root (open)
+ Hello custom container
+
+ ↳
+
+
+
+
+
+```
+
+### What about browser support?
+
+Declarative Shadow DOM is currently supported only in Chromium-based browsers and in Safari. Firefox currently does not support this, but work on supporting this [will begin soon](https://github.com/mozilla/standards-positions/issues/335#issuecomment-1714182997).
+
+### What about old browsers that will receive markup using that feature?
+
+This will create a `` element, but this template will not create shadow root automatically, so your component should do it the classic way. In conclusion, your Custom Component will work, but SSR will be "incomplete" in this browser, and the layout will be changed after JS execution.
diff --git a/client/web/divkit-examples/custom-ssr/customElement.js b/client/web/divkit-examples/custom-ssr/customElement.js
new file mode 100644
index 000000000..120555846
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/customElement.js
@@ -0,0 +1,11 @@
+class CustomContainer extends HTMLElement {
+ constructor() {
+ super();
+
+ if (!this.shadowRoot) {
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.innerHTML = 'Hello custom container';
+ }
+ }
+}
+customElements.define('custom-container', CustomContainer);
diff --git a/client/web/divkit-examples/custom-ssr/package-lock.json b/client/web/divkit-examples/custom-ssr/package-lock.json
new file mode 100644
index 000000000..8343c35a6
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/package-lock.json
@@ -0,0 +1,666 @@
+{
+ "name": "custom-ssr",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "custom-ssr",
+ "version": "1.0.0",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
+ "express": "4.18.2"
+ }
+ },
+ "node_modules/@divkitframework/divkit": {
+ "version": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
+ "resolved": "https://registry.npmjs.org/@divkitframework/divkit/-/divkit-28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb.tgz",
+ "integrity": "sha512-EEqRHNF77NYZhVsYGxQLEku9q5tYniAjFJ0UzsxM2TMwt6lLmJ96hOZbMro6wM7Fh6XuwC3Kjkc6PQdPTUuIXw==",
+ "dev": true
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "dev": true
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
+ "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dev": true,
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
+ "dev": true
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "dev": true
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dev": true,
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/client/web/divkit-examples/custom-ssr/package.json b/client/web/divkit-examples/custom-ssr/package.json
new file mode 100644
index 000000000..b4d8acd11
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "custom-ssr",
+ "version": "1.0.0",
+ "private": "true",
+ "keywords": [],
+ "scripts": {
+ "start": "node server.js"
+ },
+ "author": "",
+ "license": "UNLICENSED",
+ "devDependencies": {
+ "@divkitframework/divkit": "28.6.0-canary-f1eee8005aa682b1f02f9f9caec744f889bc1fdb",
+ "express": "4.18.2"
+ }
+}
diff --git a/client/web/divkit-examples/custom-ssr/server.js b/client/web/divkit-examples/custom-ssr/server.js
new file mode 100644
index 000000000..fd5329c90
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/server.js
@@ -0,0 +1,107 @@
+const path = require('path');
+const fs = require('fs');
+const express = require('express');
+const { render } = require('@divkitframework/divkit/server');
+
+const PORT = 3123;
+
+const template = fs.readFileSync(path.resolve(__dirname, 'template.html'), 'utf-8');
+
+function jsonFilter(str) {
+ return String(str).replace(/[<>&\u2028\u2029]/g, function(match) {
+ switch (match) {
+ case '<':
+ return '\\u003c';
+ case '>':
+ return '\\u003e';
+ case '&':
+ return '\\u0026';
+ case '\u2028':
+ return '\\u2028';
+ case '\u2029':
+ return '\\u2029';
+ }
+ return '';
+ });
+}
+
+const app = express();
+
+const json = {
+ "templates": {
+ "news_item_template": {
+ "type": "text",
+ "font_size": 16,
+ "line_height": 20,
+ "text_color": "#333",
+ "$text": "news_item_text",
+ "paddings": {
+ "left": 12,
+ "right": 12,
+ "top": 8
+ }
+ }
+ },
+ "card": {
+ "log_id": "custom_card",
+ "states": [
+ {
+ "state_id": 0,
+ "div": {
+ "type": "container",
+ "margins": {
+ "left": 16,
+ "top": 16,
+ "right": 16,
+ "bottom": 16
+ },
+ "items": [
+ {
+ "type": "custom",
+ "custom_type": "custom_container",
+ "items": [
+ {
+ "type": "news_item_template",
+ "news_item_text": "This is div-text item 1"
+ },
+ {
+ "type": "news_item_template",
+ "news_item_text": "This is div-text item 2"
+ },
+ {
+ "type": "news_item_template",
+ "news_item_text": "This is div-text item 3"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+};
+
+app.get('/', (req, res, _next) => {
+ const html = render({
+ id: 'test',
+ json,
+ customComponents: new Map([
+ ['custom_container', {
+ element: 'custom-container',
+ template: 'Hello custom container'
+ }]
+ ])
+ });
+
+ res.send(
+ template
+ .replace('[% html %]', () => html)
+ .replace('[% json %]', () => jsonFilter(JSON.stringify(json)))
+ );
+});
+
+app.use(express.static(__dirname));
+
+app.listen(PORT, () => {
+ console.log(`Server started on http://localhost:${PORT}/`);
+});
diff --git a/client/web/divkit-examples/custom-ssr/template.html b/client/web/divkit-examples/custom-ssr/template.html
new file mode 100644
index 000000000..3f0a2b352
--- /dev/null
+++ b/client/web/divkit-examples/custom-ssr/template.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+[% html %]
+
+
+
+
+
diff --git a/client/web/divkit/README.md b/client/web/divkit/README.md
index 657a835bb..ce5e578bd 100644
--- a/client/web/divkit/README.md
+++ b/client/web/divkit/README.md
@@ -335,6 +335,22 @@ Also be aware of new releases (even minor and patch versions). Built-in extensio
[Building of a new extension](../divkit-examples/extensions-handmade/)
+### customComponents
+
+`Map`, optional.
+
+Custom components can be provided using the `customComponents` map. This structure maps names from json markup to html custom elements (or web components).
+
+These components can provide any additional features that your application needs, such as map widgets, integration into social networks, YouTube videos, and so on.
+
+Custom components is supported not only on the client side, but also on the server side. See the examples below for more detailed information:
+
+[Simple custom component](../divkit-examples/custom-simple/)
+
+[Custom component with the DivKit items](../divkit-examples/custom-container/)
+
+[Custom component and SSR](../divkit-examples/custom-ssr/)
+
#### theme (EXPERIMENTAL)
`system` | `light` | `dark`
diff --git a/client/web/divkit/src/client.ts b/client/web/divkit/src/client.ts
index 60761bea2..5d3cb4ede 100644
--- a/client/web/divkit/src/client.ts
+++ b/client/web/divkit/src/client.ts
@@ -13,6 +13,7 @@ import type {
TypefaceProvider
} from '../typings/common';
import type { GlobalVariablesController } from './expressions/globalVariablesController';
+import type { CustomComponentDescription } from '../typings/custom';
export function render(opts: {
target: HTMLElement;
@@ -33,28 +34,14 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
+ customComponents?: Map | undefined;
}) {
+ const { target, hydrate, ...rest } = opts;
+
return new Root({
- target: opts.target,
- props: {
- id: opts.id,
- json: opts.json,
- globalVariablesController: opts.globalVariablesController,
- mix: opts.mix,
- customization: opts.customization,
- builtinProtocols: opts.builtinProtocols,
- extensions: opts.extensions,
- onStat: opts.onStat,
- onCustomAction: opts.onCustomAction,
- onError: opts.onError,
- onComponent: opts.onComponent,
- typefaceProvider: opts.typefaceProvider,
- platform: opts.platform,
- theme: opts.theme,
- fetchInit: opts.fetchInit,
- tooltipRoot: opts.tooltipRoot
- },
- hydrate: opts.hydrate
+ target: target,
+ props: rest,
+ hydrate: hydrate
});
}
diff --git a/client/web/divkit/src/components.js b/client/web/divkit/src/components.js
index 09693da3d..40d60ff65 100644
--- a/client/web/divkit/src/components.js
+++ b/client/web/divkit/src/components.js
@@ -13,5 +13,6 @@ module.exports.COMPONENTS = [
'slider',
'input',
'select',
- 'video'
+ 'video',
+ 'custom'
];
diff --git a/client/web/divkit/src/components/Root.svelte b/client/web/divkit/src/components/Root.svelte
index a302dbba4..d217a6d7c 100644
--- a/client/web/divkit/src/components/Root.svelte
+++ b/client/web/divkit/src/components/Root.svelte
@@ -39,6 +39,7 @@
DisappearAction,
FetchInit
} from '../../typings/common';
+ import type { CustomComponentDescription } from '../../typings/custom';
import type { AppearanceTransition, DivBaseData, Tooltip, TransitionChange } from '../types/base';
import type { SwitchElements, Overflow } from '../types/switch-elements';
import type { TintMode } from '../types/image';
@@ -70,7 +71,6 @@
} from '../expressions/globalVariablesController';
import { getUrlSchema, isBuiltinSchema } from '../utils/url';
import { TimersController } from '../utils/timers';
- import TooltipView from './tooltip/Tooltip.svelte';
export let id: string;
export let json: Partial = {};
@@ -88,6 +88,7 @@
export let typefaceProvider: TypefaceProvider = _fontFamily => '';
export let fetchInit: FetchInit = {};
export let tooltipRoot: HTMLElement | undefined = undefined;
+ export let customComponents: Map | undefined = undefined;
let isDesktop = writable(platform === 'desktop');
if (platform === 'auto' && typeof matchMedia !== 'undefined') {
@@ -972,6 +973,7 @@
typefaceProvider,
isDesktop,
isPointerFocus,
+ customComponents,
registerComponent: process.env.DEVTOOL ? registerComponentReal : undefined,
unregisterComponent: process.env.DEVTOOL ? unregisterComponentReal : undefined
});
diff --git a/client/web/divkit/src/components/custom/Custom.svelte b/client/web/divkit/src/components/custom/Custom.svelte
new file mode 100644
index 000000000..e4ccf6583
--- /dev/null
+++ b/client/web/divkit/src/components/custom/Custom.svelte
@@ -0,0 +1,98 @@
+
+
+{#if desc}
+
+
+ {#if templateContent}
+
+
+ {@html templateContent}
+
+ {/if}
+
+ {#if !hasItemsError && jsonItems}
+ {#key jsonItems}
+ {#each items as item}
+
+ {/each}
+ {/key}
+ {/if}
+
+
+{/if}
diff --git a/client/web/divkit/src/components/typeMap.ts b/client/web/divkit/src/components/typeMap.ts
index fca57a291..ef461adcd 100644
--- a/client/web/divkit/src/components/typeMap.ts
+++ b/client/web/divkit/src/components/typeMap.ts
@@ -13,6 +13,7 @@ import Slider from './slider/Slider.svelte';
import Input from './input/Input.svelte';
import Select from './select/Select.svelte';
import Video from './video/Video.svelte';
+import Custom from './custom/Custom.svelte';
export const TYPE_MAP: Record = {};
@@ -75,3 +76,7 @@ if (process.env.ENABLE_COMPONENT_SELECT || process.env.ENABLE_COMPONENT_SELECT =
if (process.env.ENABLE_COMPONENT_VIDEO || process.env.ENABLE_COMPONENT_VIDEO === undefined) {
TYPE_MAP.video = Video;
}
+
+if (process.env.ENABLE_COMPONENT_CUSTOM || process.env.ENABLE_COMPONENT_CUSTOM === undefined) {
+ TYPE_MAP.custom = Custom;
+}
diff --git a/client/web/divkit/src/context/root.ts b/client/web/divkit/src/context/root.ts
index 169456e32..2e619f26d 100644
--- a/client/web/divkit/src/context/root.ts
+++ b/client/web/divkit/src/context/root.ts
@@ -6,6 +6,7 @@ import type { MaybeMissing } from '../expressions/json';
import type { Variable, VariableType } from '../expressions/variable';
import type { TintMode } from '../types/image';
import type { Customization } from '../../typings/common';
+import type { CustomComponentDescription } from '../../typings/custom';
export const ROOT_CTX = Symbol('root');
@@ -51,6 +52,7 @@ export interface RootCtxValue {
isPointerFocus: Readable;
typefaceProvider: TypefaceProvider;
isDesktop: Readable;
+ customComponents: Map | undefined;
// Devtool
registerComponent?({
diff --git a/client/web/divkit/src/dev.ts b/client/web/divkit/src/dev.ts
index 154e2c625..240867b65 100644
--- a/client/web/divkit/src/dev.ts
+++ b/client/web/divkit/src/dev.ts
@@ -3,6 +3,7 @@ import { SizeProvider } from './extensions/sizeProvider';
import { lottieExtensionBuilder } from './extensions/lottie';
import type { DivExtensionClass } from '../typings/common';
import Lottie from 'lottie-web/build/player/lottie';
+import './devCustomComponents';
const json = {
"templates": {},
@@ -32,6 +33,23 @@ window.root = new Root({
extensions: new Map([
['size_provider', SizeProvider],
['lottie', lottieExtensionBuilder(Lottie.loadAnimation)],
+ ]),
+ customComponents: new Map([
+ ['old_custom_card_1', {
+ element: 'old-custom-card1'
+ }],
+ ['old_custom_card_2', {
+ element: 'old-custom-card2'
+ }],
+ ['new_custom_card_1', {
+ element: 'new-custom-card'
+ }],
+ ['new_custom_card_2', {
+ element: 'new-custom-card'
+ }],
+ ['new_custom_container_1', {
+ element: 'new-custom-container'
+ }]
])
}
});
diff --git a/client/web/divkit/src/devCustomComponents.ts b/client/web/divkit/src/devCustomComponents.ts
new file mode 100644
index 000000000..67c02af2a
--- /dev/null
+++ b/client/web/divkit/src/devCustomComponents.ts
@@ -0,0 +1,80 @@
+class OldCustomCard1 extends HTMLElement {
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.textContent = 'hi, i\'m old card';
+ }
+}
+class OldCustomCard2 extends HTMLElement {
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.textContent = 'and i\'m old as well';
+ }
+}
+class NewCustomCard extends HTMLElement {
+ private _counter: number;
+ private _timeout: number;
+ private _text: HTMLElement;
+
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+
+ const style = document.createElement('style');
+
+ style.textContent = `
+ .component {
+ padding: 30px;
+ font-size: 20px;
+ background: linear-gradient(85deg, #000400, #068200, #1eea00, #FF0000, #fcfa1e, #ccd49b, #7af719);
+ }
+ `;
+
+ const div = this._text = document.createElement('div');
+ div.className = 'component';
+ div.textContent = '00:00';
+
+ shadow.appendChild(style);
+ shadow.appendChild(div);
+
+ this._counter = 0;
+ this._timeout = 0;
+ this._tick();
+ }
+
+ _tick() {
+ this._timeout = window.setTimeout(() => {
+ ++this._counter;
+
+ const minutes = Math.floor(this._counter / 60);
+ const seconds = this._counter % 60;
+ this._text.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
+
+ this._tick();
+ }, 1000);
+ }
+
+ disconnectedCallback() {
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._timeout = 0;
+ }
+ }
+}
+class NewCustomContainer extends HTMLElement {
+ constructor() {
+ super();
+
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.innerHTML = '';
+ }
+}
+
+customElements.define('old-custom-card1', OldCustomCard1);
+customElements.define('old-custom-card2', OldCustomCard2);
+customElements.define('new-custom-card', NewCustomCard);
+customElements.define('new-custom-container', NewCustomContainer);
diff --git a/client/web/divkit/src/server.ts b/client/web/divkit/src/server.ts
index 2bc55c466..0e8c705b7 100644
--- a/client/web/divkit/src/server.ts
+++ b/client/web/divkit/src/server.ts
@@ -1,6 +1,7 @@
import Root from './components/Root.svelte';
import type { Platform, TypefaceProvider } from '../typings/common';
import type { GlobalVariablesController } from './expressions/globalVariablesController';
+import type { CustomComponentDescription } from '../typings/custom';
export function render(opts: {
json: unknown;
@@ -12,19 +13,10 @@ export function render(opts: {
onError?: unknown;
typefaceProvider?: TypefaceProvider;
platform?: Platform;
+ customComponents?: Map | undefined;
}) {
// Root has client-side typings, not a server one
- return (Root as any).render({
- id: opts.id,
- json: opts.json,
- globalVariablesController: opts.globalVariablesController,
- mix: opts.mix,
- customization: opts.customization,
- builtinProtocols: opts.builtinProtocols,
- onError: opts.onError,
- typefaceProvider: opts.typefaceProvider,
- platform: opts.platform
- }).html;
+ return (Root as any).render(opts).html;
}
export {
diff --git a/client/web/divkit/src/types/custom.d.ts b/client/web/divkit/src/types/custom.d.ts
new file mode 100644
index 000000000..32c24ad22
--- /dev/null
+++ b/client/web/divkit/src/types/custom.d.ts
@@ -0,0 +1,9 @@
+import type { DivBaseData } from './base';
+
+export interface DivCustomData extends DivBaseData {
+ type: 'custom';
+
+ items?: DivBaseData[];
+ custom_type: string;
+ custom_props?: object;
+}
diff --git a/client/web/divkit/typings/client-devtool.d.ts b/client/web/divkit/typings/client-devtool.d.ts
index aaad4ea5c..e616a7009 100644
--- a/client/web/divkit/typings/client-devtool.d.ts
+++ b/client/web/divkit/typings/client-devtool.d.ts
@@ -14,6 +14,7 @@ import type {
} from './common';
import type { GlobalVariablesController, Variable } from './variables';
import type { WrappedError } from './common';
+import type { CustomComponentDescription } from './custom';
export interface DivkitDebugInstance extends DivkitInstance {
getDebugVariables(): Map;
@@ -39,6 +40,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
+ customComponents?: Map | undefined;
}): DivkitDebugInstance;
export { createVariable, createGlobalVariablesController } from './variables';
diff --git a/client/web/divkit/typings/client-hydratable.d.ts b/client/web/divkit/typings/client-hydratable.d.ts
index 15dc2280f..bfb2ebbc8 100644
--- a/client/web/divkit/typings/client-hydratable.d.ts
+++ b/client/web/divkit/typings/client-hydratable.d.ts
@@ -11,6 +11,7 @@ import type {
TypefaceProvider,
FetchInit
} from './common';
+import type { CustomComponentDescription } from './custom';
import type { GlobalVariablesController } from './variables';
export function render(opts: {
@@ -32,6 +33,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
+ customComponents?: Map | undefined;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
diff --git a/client/web/divkit/typings/client.d.ts b/client/web/divkit/typings/client.d.ts
index e0b525458..cac9d38b0 100644
--- a/client/web/divkit/typings/client.d.ts
+++ b/client/web/divkit/typings/client.d.ts
@@ -11,6 +11,7 @@ import type {
TypefaceProvider,
FetchInit
} from './common';
+import type { CustomComponentDescription } from './custom';
import type { GlobalVariablesController } from './variables';
export function render(opts: {
@@ -31,6 +32,7 @@ export function render(opts: {
theme?: Theme;
fetchInit?: FetchInit;
tooltipRoot?: HTMLElement;
+ customComponents?: Map | undefined;
}): DivkitInstance;
export { createVariable, createGlobalVariablesController } from './variables';
diff --git a/client/web/divkit/typings/custom.d.ts b/client/web/divkit/typings/custom.d.ts
new file mode 100644
index 000000000..c8a41a8b0
--- /dev/null
+++ b/client/web/divkit/typings/custom.d.ts
@@ -0,0 +1,5 @@
+export interface CustomComponentDescription {
+ element: string;
+ shadowRootMode?: 'open' | 'close';
+ template?: string;
+}
diff --git a/client/web/divkit/typings/server.d.ts b/client/web/divkit/typings/server.d.ts
index 6d7e84fba..843eeca12 100644
--- a/client/web/divkit/typings/server.d.ts
+++ b/client/web/divkit/typings/server.d.ts
@@ -5,6 +5,7 @@ import type {
Customization,
TypefaceProvider
} from './common';
+import type { CustomComponentDescription } from './custom';
export function render(opts: {
json: DivJson;
@@ -15,4 +16,5 @@ export function render(opts: {
builtinProtocols?: string[];
onError?: ErrorCallback;
typefaceProvider?: TypefaceProvider;
+ customComponents?: Map | undefined;
}): string;
diff --git a/schema/div-custom.json b/schema/div-custom.json
index 72f6702a3..00b36b60d 100644
--- a/schema/div-custom.json
+++ b/schema/div-custom.json
@@ -1,10 +1,6 @@
{
"java_extends": "DivBlockWithId",
"$description": "translations.json#/div_custom",
- "platforms": [
- "android",
- "ios"
- ],
"allOf": [
{
"$ref": "div-base.json"
diff --git a/schema/div.json b/schema/div.json
index 91299c469..588350f7b 100644
--- a/schema/div.json
+++ b/schema/div.json
@@ -44,11 +44,7 @@
},
{
"$ref": "div-custom.json",
- "$description": "translations.json#/div_custom_short",
- "platforms": [
- "android",
- "ios"
- ]
+ "$description": "translations.json#/div_custom_short"
},
{
"$ref": "div-indicator.json",
diff --git a/test_data/regression_test_data/index.json b/test_data/regression_test_data/index.json
index 406fdc328..8f2b88b22 100644
--- a/test_data/regression_test_data/index.json
+++ b/test_data/regression_test_data/index.json
@@ -1564,18 +1564,10 @@
},
{
"title": "Custom container",
- "platforms": [
- "android",
- "ios"
- ],
"file": "custom-container.json"
},
{
"title": "Custom card",
- "platforms": [
- "android",
- "ios"
- ],
"file": "custom-card.json"
},
{