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 `