Skip to main content

Компонент маршрутизатора

Компонент маршрутизатора — це особливий тип вмісту, який може завантажуватися маршрутизатором, коли ми вказуємо вміст маршруту за допомогою властивостей component або componentUrl. Це має допомогти краще структурувати наші програми, зберігати речі у відповідному місці та робити багато речей швидшими, зрозумілішими та зручнішими.

Функція компонента

Компонент — це функція, яка отримує props та context і має повертати функцію відтворення. Функція візуалізації компонентів має повертати тегований літерал шаблону з HTML-вмістом компонента. Наприклад:
const MyComponent = (props, context) => {
  // some component logic
  let value = 'foo';

  // return render function
  return () => context.$h`
    <div class="page">
      <p>Value is ${value}</p>
    </div>
  `;
}

Шаблон компонента

Як згадувалося вище, функція візуалізації компонента має повертати тегований літерал шаблону з вмістом HTML компонента. У ньому є кілька важливих речей, на які варто звернути увагу. Усі самозакриваючі теги повинні бути закриті!. Якщо у вас не буде закритих самозакривних тегів, таких як <br >, <img src="/" >, <input ... >, компілятор видасть помилку. Усі порожні елементи можуть бути самозакритими:
<div class="my-div"></div>

<!-- also valid as -->
<div class="my-div" />

Реквізити компонентів

Першим аргументом, який отримує функцію компонента, є props. Цей об’єкт міститиме всі властивості, які ви передасте в методі navigate, і всі параметри маршруту. Наприклад, якщо у нас є такий маршрут:
{
  path: '/blog/:id',
  component: MyComponent
}
І коли ми переходимо до маршруту через URL-адресу /blog/34/, тоді він матиме props.id рівний «34». А також коли ми переходимо до компонента за допомогою API, наприклад:
router.navigate('/blog/34/', {
  props: {
    foo: 'bar'
  }
})
Тоді props буде таким об’єктом: { id: '34', foo: 'bar' } Також реквізити будуть містити властивості, передані користувацькому компоненту як атрибути. Якщо спеціальний компонент має такі атрибути:
<my-component foo="bar" id="25" user=${{name: 'John'}} number=${30}></my-component>
тоді $props буде:
{
  foo: 'bar',
  id: '25',
  user: {
    name: 'John'
  },
  number: 30
}

Контекст компонента

Об'єкт контексту містить багато корисних помічників:
Властивість Опис
$h
Спеціальний літерал шаблону з тегами, який потрібно використовувати для обгортання результату функції відтворення компонента та всіх записів HTML усередині:
const MyComponent = (props, { $h }) => {
  let list = ['item 1', 'item 2'];

  return () => $h`
    <div class="page">
      <ul>
        ${list.map((item) => $h`
          <li>${item}</li>
        `)}
      </ul>
    </div>
  `
}

Властивість Опис
$el
Об’єкт, у якому властивість .value містить екземпляр Dom7 з елементом HTML компонента. $el.value буде доступним лише після монтування компонента (або в будь-якій події сторінки, як-от pageInit).
const MyComponent = (props, { $el, $onMounted }) => {
  $onMounted(() => {
    $el.value.find('p').addClass('red')
  })
  // ...
}

Властивість Опис
$
Dom7 бібліотека:
const MyComponent = (props, { $, $onMounted }) => {
  $onMounted(() => {
    $('p').text('hello world')
  })
  // ...
}

Властивість Опис
$t4
Екземпляр програми Techno4
$t4.dialog.alert('Hello world!')

Властивість Опис
$store
Зберігати екземпляр. Щоб отримати додаткові відомості та приклади, перегляньте документацію Store.
$t4route
Поточний маршрут. Містить об'єкт із маршрутом query, hash, params, path і url

Властивість Опис
$t4router
Пов’язаний екземпляр маршрутизатора
$t4router.back(); //navigate back

Властивість Опис
$theme
Об’єкт із логічними властивостями md, ios і aurora, які вказують на поточну тему. Наприклад:
if ($theme.ios) { /* do something when iOS theme is active */ }
if ($theme.md) { /* do something when MD theme is active */ }
if ($theme.aurora) { /* do something when Aurora theme is active */ }

Властивість Опис
$update(callback)
Цей метод повідомляє, що цей компонент і його дочірні компоненти потрібно повторно відобразити з оновленим станом

Немає гарантії, що зміни DOM будуть застосовані негайно, тому, якщо ви покладаєтеся на DOM (наприклад, потрібно отримати вміст HTML або значення атрибутів після зміни стану), тоді передайте функцію callback як аргумент.

const MyComponent = (props, { $update, $h }) => {
  // initial state
  let value = 'foo';

  const updateValue = () => {
    // update local state
    value = 'foo2';
    // call $update method
    $update();
  }

  return () => $h`
    <div class="page">
      <p>Value is ${value}</p>
      <button @click=${updateValue}>Update Value</button>
    </div>
  `;
}

Властивість Опис
$ref(initialValue)
Цей метод створює реактивну "змінну", яка після оновлення автоматично оновлює компонент без необхідності викликати метод $update(). Він повертає об’єкт із властивістю value, якому має бути присвоєно нове значення.

Немає гарантії, що зміни DOM будуть застосовані негайно, тому, якщо ви покладаєтеся на DOM (наприклад, потрібно отримати вміст HTML або значення атрибутів після зміни стану), тоді передайте функцію callback як аргумент.

const MyComponent = (props, { $ref, $h }) => {
  // create reactive object
  const someVar = $ref('foo'); //-> { value: 'foo' }

  const updateValue = () => {
    // update "value" property of the reactive object
    someVar.value = 'bar';
  }

  return () => $h`
    <div class="page">
      <p>Value is ${someVar.value}</p>
      <button @click=${updateValue}>Update Value</button>
    </div>
  `;
}

Властивість Опис
$tick(callback)
Ви також можете використовувати цей метод, якщо ви покладаєтеся на DOM і вам потрібно переконатися, що стан компонента та DOM оновлені після виклику методів $update(). Переданий callack буде виконано під час оновлення DOM. Цей метод повертає Promise, який також буде вирішено під час оновлення DOM. Тож ви можете використовувати це так:
$update();

$tick(function () {
  console.log('DOM and state updated');
});

// Or as Promise
$tick().then(() => {
  console.log('DOM and state updated');
})

// Or in async function/method as:
await $tick();
console.log('DOM and state updated');

Властивість Опис
$t4ready(callback)
Цей метод потрібно використовувати лише тоді, коли ви використовуєте основний компонент програми, щоб обов’язково викликати Techno4 API під час ініціалізації програми.
const AppComponent = (props, { $t4, $t4ready }) => {
  $t4ready(() => {
    // now it is safe to call Techno4 APIs
    $t4.dialog.alert('Hello!');
  });

  // ...

Події

Властивість Опис
$on
Функція для приєднання обробників подій DOM до кореневого елемента компонента

Такі обробники подій будуть автоматично відключені, коли компонент буде знищено

const MyComponent = (props, { $on }) => {
  // attach 'pageInit' event handler
  $on('pageInit', (e, page) => {
    console.log(page.name)
  });
  // ...
}

Властивість Опис
$once
Функція для приєднання обробників подій DOM до кореневого елемента компонента. Те саме, що $on, але такі обробники виконуватимуться лише один раз.

Властивість Опис
$emit(event, data)
Функція запуску настроюваних подій DOM у багаторазово використовуваних настроюваних компонентах:
const MyComponent = (props, { $emit }) => {
  // emits custom event
  $emit('myevent')
  // ...
}
І в іншому батьківському компоненті:
<my-component @myevent=${doSomething} />

Хуки життєвого циклу

Властивість Опис
$onBeforeMount
Викликається безпосередньо перед тим, як компонент буде додано до DOM

Властивість Опис
$onMounted
Викликається одразу після додавання компонента до DOM
const MyComponent = (props, { $onMounted }) => {
  // do something when component mounted
  $onMounted(() => {
    console.log('component mounted')
  });
  // ...
}

Властивість Опис
$onBeforeUpdate
Викликається відразу після компонента перед тим, як VDOM буде виправлено/оновлено
$onUpdated
Викликається одразу після виправлення/оновлення компонента VDOM
$onBeforeUnmount
Викликається безпосередньо перед тим, як компонент буде демонтовано (від’єднано від DOM)
$onUnmounted
Викликається, коли компонент демонтовано та знищено

Отже, приклад маршруту з компонентом сторінки може виглядати так:
routes = [
  // ...
  {
    path: '/some-page/',
    // Component
    component: (props, { $h, $t4, $on }) => {
      const title = 'Component Page';
      const names = ['John', 'Vladimir', 'Timo'];

      const openAlert = () => {
        $t4.dialog.alert('Hello world!');
      }

      $on('pageInit', (e, page) => {
        // do something on page init
      });
      $on('pageAfterOut', (e, page) => {
        // page has left the view
      });

      return () => $h`
        <div class="page">
          <div class="navbar">
            <div class="navbar-bg"></div>
            <div class="navbar-inner">
              <div class="title">${title}</div>
            </div>
          </div>
          <div class="page-content">
            <a @click=${openAlert} class="red-link">Open Alert</a>
            <div class="list simple-list">
              <ul>
                ${names.map((name) => $h`
                  <li>${name}</li>
                `)}
              </ul>
            </div>
          </div>
        </div>
      `;
    },
  },
  // ...
]

Події сторінки компонента

Обробники подій сторінки компонента можна передати в обробник подій компонента $on. Це звичайні події сторінки DOM. Оскільки це події DOM, вони приймають event як перший аргумент, а дані сторінки як другий аргумент. Єдина відмінність від звичайних подій DOM полягає в тому, що ім’я обробника подій має бути вказано у форматі CamelCase (page:init -> pageInit):
const MyComponent = (props, { $on }) => {
  $on('pageMounted', (e, page) => {
    console.log('page mounted');
  });
  $on('pageInit', (e, page) => {
    console.log('page init');
  });
  $on('pageBeforeIn', (e, page) => {
    console.log('page before in');
  });
  $on('pageAfterIn', (e, page) => {
    console.log('page after in');
  });
  $on('pageBeforeOut', (e, page) => {
    console.log('page before out');
  });
  $on('pageAfterOut', (e, page) => {
    console.log('page after out');
  });
  $on('pageBeforeUnmount', (e, page) => {
    console.log('page before unmount');
  });
  $on('pageBeforeRemove', (e, page) => {
    console.log('page before remove');
  });
}

Обробка подій DOM

Зверніть увагу на додатковий атрибут @ у шаблоні компонента. Це скорочений метод для призначення слухача події зазначеному елементу. Зазначений обробник подій шукатиметься в області компонента. Таке значення атрибута обробника подій має бути функцією:
const MyComponent = (props, { $h, $update }) => {
  let value = 10;
  const addValue = (number) => {
    value += number;
    $update();
  }
  const onClick = () => {
    console.log('click');
  }

  return () => $h`
    <div class="page">
      <!-- pass function to attribute -->
      <button @click=${onClick}>Button</button>

      <!-- also work -->
      <button @click=${() => onClick()}>Button</button>

      <!-- will not work, attribute value "onClick" is just a string -->
      <button @click="onClick">Button</button>

      <!-- passing dynamic data will work as expected -->
      <button @click=${() => addValue(15)}>Button</button>
    </div>
  `
}
Обробники подій обробляються лише під час початкового рендерингу або для елементів, виправлених за допомогою VDOM. Якщо ви додасте такий елемент в DOM вручну, він не працюватиме!
const MyComponent = (props, { $h, $on }) => {
  const onClick = () => {
    console.log('click');
  }

  $on('pageInit', (e, page) => {
    // this won't work
    page.$el.append('<a @click="onClick">Link</a>');
  });

  return () => $h`
    <div class="page">
    </div>
  `
}

Кореневий елемент компонента

Шаблон компонента або функція відтворення має повертати лише один елемент HTML. І це має бути елемент, який підтримується маршрутизатором:

Якщо ви завантажуєте сторінки як компонент маршрутизатора, компонент маршрутизатора повинен повертати елемент Page:

<template>
  <div class="page">
    ...
  </div>
</template>
Якщо ви завантажуєте модальний (Routable Modals) як компонент маршрутизатора, компонент маршрутизатора повинен повернути цей модальний елемент:
<template>
  <div class="popup">
    ...
  </div>
</template>
Якщо ви завантажуєте панель (Routable Panels) як компонент маршрутизатора, то компонент маршрутизатора повинен повертати елемент панелі:
<template>
  <div class="panel panel-left panel-cover">
    ...
  </div>
</template>
Якщо ви завантажуєте вміст вкладки (Routable Tabs) як компонент маршрутизатора, тоді компонент маршрутизатора має повернути дочірній елемент Tab, який буде вставлено всередину маршрутизованої вкладки:
<template>
  <div class="some-element">
    ...
  </div>
</template>

Однофайловий компонент

Не дуже зручно вказувати всі складові маршрути в одному масиві routes, особливо якщо у нас багато таких маршрутів. Ось чому ми можемо замість цього використовувати componentUrl і помістити компонент в один файл:
routes = [
  ...
  {
    path: '/some-page/',
    componentUrl: './some-page.t4',
  },
  ..
];
Також в some-page.t4:
<!-- component template, uses same tagged template literals -->
<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">${title}</div>
      </div>
    </div>
    <div class="page-content">
      <a @click=${openAlert}>Open Alert</a>
      <div class="list simple-list">
        <ul>
          ${names.map((name) => $h`
            <li>${name}</li>
          `)}
        </ul>
      </div>
    </div>
  </div>
</template>
<!-- component styles -->
<style>
  .red-link {
    color: red;
  }
</style>
<!-- rest of component logic -->
<script>
  // script must return/export component function
  export default (props, { $t4, $on }) => {
    const title = 'Component Page';
    const names = ['John', 'Vladimir', 'Timo'];

    const openAlert = () => {
      $t4.dialog.alert('Hello world!');
    }

    $on('pageInit', () => {
      // do something on page init
    });
    $on('pageAfterOut', () => {
      // page has left the view
    });

    // component function must return render function
    return $render;
  }
</script>
Тепер стало набагато чистіше. Теги <template> і <style>будуть автоматично перетворені на ті самі властивості експортованого компонента. Обов’язковим є return $render у кінці функції компонента, оскільки його буде замінено аналізатором із вмістом тегу <template>.

Використання з Webpack і Vite

Для Webpack існує спеціальний плагін Techno4-loader, який дозволяє об’єднувати однофайлові компоненти в основний пакет і не використовувати XHR (наприклад, componentUrl) для кожного разу завантажувати та аналізувати файли компонентів.

Для Vite.js також існує спеціальний плагін rollup-plugin-Techno4 для об’єднання однофайлових компонентів.

Ці плагіни аналізують файл однофайлового компонента та перетворюють його на простий об’єкт JS під час процесу об’єднання. Отже, потенційно це може збільшити продуктивність програми, оскільки не буде синтаксичного аналізу та компіляції під час виконання.

Коли плагін налаштовано, нам потрібно зберігати однофайлові компоненти у файлах .t4 (або у .t4.html для Webpack) і використовувати export default для експорту компонентів:
<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  export default () => {
    let foo = 'bar';

    const doThis = () => {
      // ...
    }

    return $render;
  }
</script>
Також можна імпортувати необхідні залежності та стилі:
<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  import './path/to/some-styles.css';
  import utils from './path/to/utils.js';

  export default () => {
    let foo = 'bar';
    let now = utils.now();

    const doThis = () => {
      // ...
    }

    return $render;
  }
</script>
А потім ми можемо імпортувати його та додати до маршрутів:
// routes.js

import NewsPage from './path/to/news.t4';
import ServicesPage from './path/to/services.t4';

export default [
  {
    path: '/news/',
    component: NewsPage,
  },
  {
    path: '/services/',
    component: ServicesPage,
  }
]

JSX

Літерали шаблону не мають належного підсвічування синтаксису в документах HTML. Але при використанні з webpack або Vite також можна писати компоненти в синтаксисі JSX. Щоб це запрацювало, нам потрібно зберегти компоненти у файлах .t4.jsx і записати їх за допомогою JSX:
export default (props, { $update }) => {
  let value = 10;
  const items = ['Item 1', 'Item 2'];

  const addValue = (number) => {
    value += number;
    $update();
  }

  //- render function should returns JSX
  return () => (
    <div class="page">
      <p>The value is {value}</p>
      <p>
        {/* JSX doesn't support @ in attribute name so event handlers should start from "on" */}
        <button onClick={() => addValue(10)}>Add Value</button>
      </p>
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  )
}
І імпортуйте їх таким же чином в routes.js:
import NewsPage from './path/to/news.t4.jsx';
import ServicesPage from './path/to/services.t4.jsx';

export default [
  {
    path: '/news/',
    component: NewsPage,
  },
  {
    path: '/services/',
    component: ServicesPage,
  }
]

Віртуальний DOM

Віртуальний DOM і всі функції, пов’язані з VDOM, доступні з Techno4 версії 3.1.0.

Віртуальний DOM (VDOM) — це концепція програмування, де ідеальне або «віртуальне» представлення інтерфейсу користувача зберігається в пам’яті та синхронізується з «реальним» DOM. Це дозволяє нам представити вигляд нашої програми як функцію її стану.

Бібліотека VDOM називається Snabbdom, оскільки вона надзвичайно легка, швидка та чудово підходить для середовища Techno4.

Отже, як працює VDOM-рендеринг компонента маршрутизатора Techno4? Шаблон компонента перетворюється на VDOM замість безпосереднього вставлення в DOM. Пізніше, коли стан компонента змінюється, він створює новий VDOM і порівнює його з попереднім VDOM. І на основі цієї різниці він виправляє реальний DOM шляхом зміни лише елементів і атрибутів, які потрібно змінити. І все це відбувається автоматично!

Давайте розглянемо приклад компонента профілю користувача, який автоматично оновлюватиме макет, коли ми запитуватимемо дані користувача:

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">Profile</div>
      </div>
    </div>
    <div class="page-content">
      ${user && $h`
        <!-- Show user list when it is loaded -->
        <div class="list simple-list">
          <ul>
            <li>First Name: ${user.firstName}</li>
            <li>Last Name: ${user.lastName}</li>
            <li>Age: ${user.age}</li>
          </ul>
        </div>
      `}
      ${!user && $h`
        <!-- Otherwise show preloader -->
        <div class="block block-strong text-align-center">
          <div class="preloader"></div>
        </div>
      `}
    </div>
  </div>
</template>
<script>
  export default (props, { $on, $t4, $update }) => {
    // empty initial user data
    let user = null;

    $on('pageInit', () => {
      // request user data on page init
      $t4.request.get('https://api.website.com/get-user-profile').then((res) => {
        // update user with new data
        user = res.data;
        // trigger re-render
        $update();
      });
    })

    return $render;
  }
</script>
Зауважте, що пряме призначення стану компонента не призведе до оновлення макета. Використовуйте $update щоразу, коли вам потрібно оновити макет компонента!

Ключі в списках і компонентах автоініціалізації

Коли VDOM оновлює список елементів, за замовчуванням він використовує стратегію «виправлення на місці». Якщо порядок елементів даних змінився, замість того, щоб перемістити елементи DOM відповідно до порядку елементів, він виправить кожен елемент на місці та переконається, що він відображає те, що має бути відображено за цим конкретним індексом.

Цей режим за замовчуванням є ефективним, але придатним лише тоді, коли вихідні дані візуалізації не залежать від стану дочірнього компонента чи тимчасового стану DOM (наприклад, вхідні значення форми).

Щоб дати VDOM підказку, щоб він міг відстежувати ідентичність кожного вузла, і таким чином повторно використовувати та змінювати порядок існуючих елементів, вам потрібно надати унікальний атрибут key для кожного елемента.

Під час відтворення списків ідеальним значенням для key буде унікальний ідентифікатор кожного елемента:
<template>
  ...
  <ul>
    ${items.map((item) => $h`
      <li key=${item.id}>...</li>
    `)}
  </ul>
  ...
</template>
<script>
  export default () => {
    const items = [
      {
        id: 1,
        title: 'Item A'
      },
      {
        id: 2,
        title: 'Item B'
      },
    ];

    return $render;
  }
</script>
Те ж саме з автоматично ініціалізованими компонентами, такими як Range Slider, Gauge та інші, які мають бути автоматично ініціалізовані (якщо вони мають range-slider-init, gauge-init), коли вони додаються до DOM, і автоматично знищуються, коли вони видаляються з DOM. Таким чином, такі елементи також повинні бути ідентифіковані унікальними ключами.
<template>
  <div class="page">
    ...
    <div class="page-content">
      ${gaugeVisible && $h`
        <!-- must have unique key -->
        <div key="gauge" class="gauge gauge-init" data-type="circle"
          data-value="0.60"
          data-value-text="60%"
          data-value-text-color="#ff9800"
          data-border-color="#ff9800"
        ></div>
      `}
      ...
      <a href="#" class="button" @click=${showGauge}>Show Gauge</a>
    </div>
  </div>
</template>

<script>
  export default (props, { $update }) => {
    let gaugeVisible = false;

    const showGauge = () => {
      gaugeVisible = true;
      $update();
    }

    return $render;
  }
</script>
Зауважте, що key атрибут має бути унікальним для одного компонента.

Якщо атрибут key не вказано, а елемент має атрибут id, тоді атрибутid використовуватиметься як унікальний ключ віртуального вузла.

innerHTML

Якщо нам потрібно вставити рядок HTML (наприклад, отриманий з кінцевої точки API), нам потрібно використовувати спеціальний prob/attribute елемента innerHTML:
<template>
  <div class="page">
    ...
    <div class="block" innerHTML=${customHTML}></div>
  </div>
</template>
<script>
  export default (props) => {
    const customHTML = '<p>Hello <b>World!</b></p>';

    return $render;
  }
</script>
Використання innerHTML для елемента замінить усі його дочірні елементи. Вміст HTML, переданий у innerHTML, є лише рядком, і, наприклад, обробники подій компонента (наприклад, атрибут @click) не працюватимуть.

Основний компонент програми

Можна створити весь макет програми як компонент.

Зауважте, що через реалізацію VDOM настійно рекомендується додавати унікальний id або атрибут key до кожного автоматично ініціалізованого представлення (перегляд із view-init класом ):

Щоб увімкнути його, спочатку ми повинні залишити кореневий елемент додатка порожнім у index.html:
<body>
  <!-- empty app root element -->
  <div id="app"></div>
</body>
Потім нам потрібно створити основний компонент додатка, наприклад Single File Component за допомогою Vite:
<!-- app.t4 -->
<template>
  <div id="app">
    ${loggedIn.value && $h`
      <div class="panel panel-left panel-reveal panel-init">
        <!-- every View has unique ID attribute -->
        <div class="view view-init" id="view-panel" data-url="/panel/"></div>
      </div>
      <div class="view view-main view-init" id="view-main" data-url="/"></div>
    `}
    ${!loggedIn.value && $h`
      <div class="login-screen modal-in">
        <div class="view view-init" id="view-auth" data-url="/auth/"></div>
      </div>
    `}
  </div>
</template>
<script>
  export default (props, { $store }) => {
    const loggedIn = $store.getters.loggedIn;

    return $render;
  }
</script>
Нарешті, коли ми запускаємо Techno4, нам потрібно вказати компонент додатка під час ініціалізації:
// import main app component
import App from './path/to/app.t4';

var app = new Techno4({
  // specify main app component
  component: App,
})
Або, якщо ми не використовуємо webpack, ми також можемо завантажити його через XHR:
var app = new Techno4({
  // load main app component
  componentUrl: './path/to/app.t4',
})
Також зауважте, що основний компонент програми буде змонтовано (додано до DOM) ДО завершення процесу ініціалізації програми. Отже, якщо вам потрібно негайно викликати Techno4 API, використовуйте $t4ready callback:
<template>
  <div id="app">
    ...
  </div>
</template>
<script>
  export default (props, { $t4ready, $t4 }) => {
    $t4ready(() => {
      // now it is safe to call Techno4 APIs
      $t4.dialog.alert('Hello!');
    })
  }
</script>

Спеціальні компоненти

Реєстрація компонентів

Є можливість створювати власні багаторазові компоненти. Нам потрібно зробити це ДО ініціалізації Techno4 за допомогою наступного методу: Techno4.registerComponent(tagName, component)- зареєструвати спеціальний компонент
tagName - string. Назва тегу компонента, напр. my-component (буде використано як <my-component>). Назва тегу спеціального компонента має містити символ дефіса/тире "-" component - object or class. Функція компонента

Зауважте, що на даний момент користувальницькі компоненти можна використовувати лише в компонентах маршрутизатора (компоненти, завантажені маршрутизатором).

Techno4.registerComponent(
  // component name
  'my-list-item',

  // component function
  (props, { $h }) => {
    let foo = 'bar';

    return () => $h`
      <li class="item-content" id="${props.id}">...</li>
    `
  }
)
І використовуйте його в інших компонентах, таких як:
<div class="list">
  <ul>
    <my-list-item id="item-1"></my-list-item>
  </ul>
</div>
Зауважте, що атрибути, передані настроюваному елементу компонента, доступні в атрибутах props.

Локальні компоненти

Локальні спеціальні компоненти можна створювати в компонентах:
<template>
  <ul>
    <!-- use tag names as variables -->
    <${ListItem} title="Item 1" />
    <${ListItem} title="Item 2" />
    <${ListItem} title="Item 3" />
  </ul>
</template>
<script>
  // create local component
  const ListItem = (props, { $h }) => {
    return () => $h`<li>${props.title}</li>`;
  }

  // export main component
  export default () => {
    return $render;
  }
</script>
Або їх можна імпортувати:
<template>
  <ul>
    <!-- use tag names as variables -->
    <${ListItem} title="Item 1" />
    <${ListItem} title="Item 2" />
    <${ListItem} title="Item 3" />
  </ul>
</template>
<script>
  // import component
  import ListItem from 'path/to/list-item.t4';

  // export main component
  export default () => {
    return $render;
  }
</script>
З JSX:
const ListItem = (props) => {
  return (
    <li>{props.title}</li>
  )
}
/* or
import ListItem from 'path/to/list-item.t4.jsx'
*/

export default () => {
  return () => (
    <ul>
      <ListItem title="Item 1" />
      <ListItem title="Item 2" />
      <ListItem title="Item 3" />
    </ul>
  )
}
У JSX його можна створити всередині основного компонента:
export default () => {

  const ListItem = (props) => {
    return (
      <li>{props.title}</li>
    )
  }

  return () => (
    <ul>
      <ListItem title="Item 1" />
      <ListItem title="Item 2" />
      <ListItem title="Item 3" />
    </ul>
  )
}

Події

Ви можете призначати події DOM для спеціального компонента в шаблонах із тим самим синтаксисом @{event}. Обробник подій буде фактично приєднаний до кореневого елемента спеціального компонента.
<template>
  <div class="page">
    ...
    <my-button @click="onClick">Click Me</my-button>
  </div>
</template>
<script>
  return {
    // ...
    methods: {
      onClick: function(e) {
        console.log('clicked');
      }
    },
    // ...
  }
</script>

Слоти

Якщо нам потрібно передати дочірні елементи (або текст) до спеціального компонента, нам потрібно використовувати слоти. Реалізація слотів тут подібна до слотів веб-компонентів.

За допомогою тегу slot ми вказуємо, де слід розмістити дочірні компоненти. Наприклад, шаблон компонента my-button:

<a class="button button-fill">
  <slot></slot>
</a>
Тоді можна використовувати так:
<my-button>Click Me</my-button>
Щоб вказати значення слота за замовчуванням (якщо дочірні елементи не передані), ми просто вставляємо його в тег <slot>:
<a class="button button-fill">
  <slot>Default Button Text</slot>
</a>
Щоб розподілити елементи по макету компонентів, ми можемо використовувати іменні слоти. Наприклад, шаблон компонента my-container:
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
І ми можемо використовувати його наступним чином:
<my-container>
  <h1 slot="header">Title</h1>

  <p>Text for main content.</p>
  <p>More text for main content.</p>

  <p slot="footer">Footer content</p>
</my-container>
І результат компонента буде таким:
<div class="container">
  <header>
    <h1>Title</h1>
  </header>
  <main>
    <p>Text for main content.</p>
    <p>More text for main content.</p>
  </main>
  <footer>
    <p>Footer content</p>
  </footer>
</div>

Рецепти шаблонів

Умовний рендеринг

Для реалізації умов у JavaScript ми зазвичай використовуємо оператори if (if-else). Усередині шаблонів і JSX ми не можемо використовувати їх напряму і повинні використовувати оператори JavaScript.

if

Для оператора if ми повинні використовувати логічний оператор AND (&&):
<template>
  <div class="page">
    ${someVar && $h`
      <p>Text will be visible when "someVar" is truthy</p>
    `}

    ${someVar === 1 && $h`
      <p>Text will be visible when "someVar" equals to 1</p>
    `}
  </div>
</template>
<script>
  export default () => {
    const someVar = 1;

    return $render;
  }
</script>
Те саме з використанням JSX:
export default () => {
  const someVar = 1;

  return () => (
    <div class="page">
      {someVar && (
        <p>Text will be visible when "someVar" is truthy</p>
      )}

      {someVar === 1 && (
        <p>Text will be visible when "someVar" equals to 1</p>
      )}
    </div>
  )
}

if-else

Для if-else ми можемо використовувати тернарний оператор (?:) або комбінацію операторів && та ! :
<template>
  <div class="page">
    ${someVar ? $h`
      <p>Text will be visible when "someVar" is truthy</p>
    ` : $h`
      <p>Text will be visible when "someVar" is falsy</p>
    `}

    {someVar && (
      <p>Text will be visible when "someVar" is truthy</p>
    )}
    {!someVar && (
      <p>Text will be visible when "someVar" is falsy</p>
    )}
  </div>
</template>
<script>
  export default () => {
    const someVar = 1;

    return $render;
  }
</script>
Те саме з використанням JSX:
export default () => {
  const someVar = 1;

  return () => (
    <div class="page">
      {someVar ? (
        <p>Text will be visible when "someVar" is truthy</p>
      ) : (
        <p>Text will be visible when "someVar" is falsy</p>
      )}

      {someVar && (
        <p>Text will be visible when "someVar" is truthy</p>
      )}
      {!someVar && (
        <p>Text will be visible when "someVar" is falsy</p>
      )}

    </div>
  )
}

Відображення масиву на елементи

Щоб зіставити масив з елементами, ми використовуємо метод Array .map():
<template>
  <div class="page">
    <ul>
    ${items.map((item) => $h`
      <li>${item}</li>
    `)}
    </ul>
  </div>
</template>
<script>
  export default () => {
    const items = [
      'item 1',
      'item 2',
      'item 3',
    ];

    return $render;
  }
</script>
Те саме з використанням JSX:
export default () => {
  const items = [
    'item 1',
    'item 2',
    'item 3',
  ];

  return () => (
    <div class="page">
      <ul>
        {items.map((item) => (
          <li>{item}</li>
        ))}
      </ul>
    </div>
  )
}