MetaFor - v0.3.1
    Preparing search index...

    MetaFor - v0.3.1

    MetaFor

    MetaFor — это современный VanillaJS фреймворк для создания real-time веб-приложений на основе контекстно-ориентированного конечного автомата с декларативным API, типобезопасностью и реактивностью. Работает как на клиенте, так и на сервере.

    • Контекстно-ориентированный конечный автомат — состояния и переходы зависят от контекста
    • Universal JavaScript — работает как на клиенте, так и на сервере
    • Real-time обновления — мгновенная реакция на изменения состояния без перезагрузки
    • Типобезопасность — полная типизация TypeScript для всех компонентов
    • Реактивность — автоматическое обновление UI при изменении состояния
    • Процессы — действия с обработкой успеха/ошибок (асинхронные и синхронные)
    • Реакции — декларативные фильтры для обработки внешних событий
    • Шаблонизация — современный HTML template API с директивами
    • Веб-компоненты — нативная поддержка Custom Elements
    • Zero-build — работает без сборщиков и компиляции
    MetaFor("counter")
    .context((types) => ({
    count: types.number.required(0),
    isLoading: types.boolean.required(false),
    }))
    .states({
    idle: { loading: {} },
    loading: {
    success: { count: { gt: 0 } },
    error: { isLoading: false },
    },
    success: { idle: {} },
    error: { idle: {} },
    })
    .core()
    .processes((process) => ({
    loading: process()
    .action(async ({ context }) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return { count: context.count + 1 }
    })
    .success(({ update, data }) => update({ count: data.count, isLoading: false }))
    .error(({ update }) => update({ isLoading: false })),
    }))
    .reactions()
    .view({
    render: ({ context, html }) => html`
    <div>
    <h1>Счётчик: ${context.count}</h1>
    <button ?disabled=${context.isLoading}>${context.isLoading ? "Загрузка..." : "Увеличить"}</button>
    </div>
    `,
    })

    MetaFor состоит из нескольких ключевых компонентов:

    Контекст — это типизированное состояние компонента, которое автоматически обновляет UI при изменениях.

    .context((types) => ({
    // Обязательные поля
    name: types.string.required("Anonymous"),
    age: types.number.required(18),
    isActive: types.boolean.required(false),

    // Опциональные поля
    email: types.string.optional(),
    avatar: types.string.optional(),

    // Массивы
    tags: types.array.required([]),

    // Enum
    status: types.enum.required(["pending", "active", "blocked"]),
    }))

    Поддерживаемые типы:

    • string — строки
    • number — числа
    • boolean — булевы значения
    • array — массивы
    • enum — перечисления

    Состояния определяют возможные переходы автомата с условиями.

    .states({
    guest: {
    // Переход в user при выполнении условий
    user: {
    name: { length: { min: 2 } },
    email: { pattern: /@/ }
    }
    },
    user: {
    // Переход в admin при isAdmin: true
    admin: { isAdmin: true },
    // Переход в guest при logout: true
    guest: { logout: true }
    },
    admin: {
    user: { isAdmin: false }
    }
    })

    Условия переходов:

    Для строк:

    name: {
    eq: "admin", // равно
    startsWith: "user", // начинается с
    endsWith: "admin", // заканчивается на
    include: "test", // содержит подстроку
    pattern: /^[a-z]+$/, // регулярное выражение
    length: { min: 3, max: 20 } // длина
    }

    Для чисел:

    age: {
    eq: 18, // равно
    gt: 0, // больше
    gte: 18, // больше или равно
    lt: 100, // меньше
    lte: 65, // меньше или равно
    between: [18, 65] // диапазон
    }

    Для булевых значений:

    isActive: {
    eq: true, // равно
    notEq: false // не равно
    }

    Для массивов:

    tags: {
    length: { min: 1 }, // длина
    includes: "admin", // содержит элемент
    isEmpty: false // не пустой
    }

    Процессы — это действия с обработкой успеха и ошибок. Они могут быть как асинхронными, так и синхронными.

    .processes((process) => ({
    login: process({
    title: "Авторизация",
    description: "Процесс входа пользователя"
    })
    .action(async ({ context }) => {
    // Основная логика
    const response = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({
    email: context.email,
    password: context.password
    })
    })

    if (!response.ok) {
    throw new Error('Ошибка авторизации')
    }

    return await response.json()
    })
    .success(({ update, data }) => {
    // Обработка успеха
    update({
    isAuthenticated: true,
    user: data.user,
    error: ""
    })
    })
    .error(({ update, error }) => {
    // Обработка ошибки
    update({
    error: error.message,
    isAuthenticated: false
    })
    }),

    logout: process()
    .action(({ context }) => {
    // Синхронное действие
    localStorage.removeItem('token')
    return { success: true }
    })
    .success(({ update }) => {
    update({
    isAuthenticated: false,
    user: null,
    error: ""
    })
    })
    }))

    Реакции позволяют обрабатывать внешние события через декларативные фильтры.

    .reactions((reaction) => [
    [
    ["idle", "loading"], // Состояния, в которых активна реакция
    reaction({ title: "Обработка сообщений" })
    .filter({
    tag: "user", // Фильтр по тегу
    op: "replace", // Фильтр по операции
    path: "/context", // Фильтр по пути
    value: { gt: 0 } // Фильтр по значению
    })
    .equal(({ update, context, meta, patch }) => {
    // Обработка события
    update({
    lastMessage: patch.value,
    messageCount: context.messageCount + 1
    })
    })
    ],
    [
    ["idle", "loading", "success", "error"], // Все состояния
    reaction()
    .filter({ tag: "system" })
    .equal(({ update }) => {
    update({ systemNotification: true })
    })
    ]
    ])

    Фильтры реакций:

    • tag — фильтр по тегу сообщения
    • index — фильтр по индексу
    • timestamp — фильтр по временной метке
    • op — фильтр по операции (replace, add, remove, test)
    • path — фильтр по пути (/context, /state, /)
    • value — фильтр по значению с поддержкой всех типов условий

    Представление определяет UI компонента с использованием HTML template API.

    .view({
    render: ({ context, html, update }) => html`
    <div class="user-profile">
    <h1>${context.name}</h1>

    ${context.isLoading
    ? html`<div class="loading">Загрузка...</div>`
    : html`
    <form @submit=${(e) => {
    e.preventDefault()
    update({ isLoading: true })
    }}>
    <input
    .value=${context.email}
    @input=${(e) => update({ email: e.target.value })}
    placeholder="Email"
    />
    <button type="submit" ?disabled=${!context.email}>
    Сохранить
    </button>
    </form>
    `
    }

    ${context.error
    ? html`<div class="error">${context.error}</div>`
    : null
    }
    </div>
    `,

    style: ({ css }) => css`
    .user-profile {
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 8px;
    }

    .loading {
    color: #666;
    font-style: italic;
    }

    .error {
    color: red;
    margin-top: 10px;
    }

    input {
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-right: 10px;
    }

    button {
    padding: 8px 16px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    }

    button:disabled {
    background: #ccc;
    cursor: not-allowed;
    }
    `
    })

    Директивы HTML:

    • @event — обработчики событий
    • ?attribute — булевы атрибуты
    • .property — свойства элементов
    • ${ref()} — ссылки на элементы
    • ${when(condition, template)} — условный рендеринг
    • ${repeat(items, template)} — циклы
    • ${map(items, fn)} — преобразование массивов

    MetaFor поддерживает передачу контекста от родительского компонента к дочернему через специальный атрибут context.

    // Родительский компонент
    MetaFor("parent")
    .context((types) => ({
    parentMessage: types.string.required("Hello from parent"),
    parentCount: types.number.required(42),
    }))
    .states({ idle: {} })
    .core()
    .processes()
    .reactions()
    .view({
    render: ({ context, html }) => html`
    <div>
    <h1>Родитель: ${context.parentMessage}</h1>
    <metafor-child
    context=${{
    message: context.parentMessage,
    count: context.parentCount,
    }}></metafor-child>
    </div>
    `,
    })

    // Дочерний компонент
    MetaFor("child")
    .context((types) => ({
    message: types.string.required("default message"),
    count: types.number.required(0),
    }))
    .states({ idle: {} })
    .core()
    .processes()
    .reactions()
    .view({
    render: ({ context, html }) => html`
    <div>
    <p>Сообщение: ${context.message}</p>
    <p>Счетчик: ${context.count}</p>
    </div>
    `,
    })

    Особенности передачи контекста:

    • Контекст передается как объект через атрибут context=${object}
    • При первой отрисовке контекст устанавливается без дополнительных сообщений
    • При обновлении контекста родителя автоматически обновляется контекст ребенка
    • Компонент-ребенок должен быть уже зарегистрирован в MetaFor
    • Поддерживается реактивное обновление при изменении контекста родителя

    Создает новый экземпляр MetaFor с указанным тегом компонента.

    const component = MetaFor("my-component")
    

    MetaFor использует цепочку методов для конфигурации:

    MetaFor("example")
    .context(schema) // Схема контекста
    .states(config) // Конфигурация состояний
    .core({}) // Инициализация ядра
    .processes(config) // Конфигурация процессов
    .reactions(config) // Конфигурация реакций
    .view(config) // Конфигурация представления
    .context((types) => ({
    // Обязательные поля
    field: types.string.required(defaultValue),
    field: types.number.required(defaultValue),
    field: types.boolean.required(defaultValue),
    field: types.array.required(defaultValue),
    field: types.enum.required(values),

    // Опциональные поля
    field: types.string.optional(),
    field: types.number.optional(),
    field: types.boolean.optional(),
    field: types.array.optional(),
    field: types.enum.optional(values),
    }))
    .states({
    stateName: {
    nextState: conditions,
    anotherState: conditions,
    }
    })
    .processes((process) => ({
    processName: process(config?)
    .action(fn)
    .success(handler)
    .error(handler)
    }))
    .reactions((reaction) => [
    [
    ["state1", "state2"],
    reaction(config?)
    .filter(conditions)
    .equal(handler)
    ]
    ])
    .view({
    render: ({ context, html, update, ref }) => html`...`,
    style: ({ css }) => css`...`
    })
    MetaFor("async-counter")
    .context((types) => ({
    count: types.number.required(0),
    isLoading: types.boolean.required(false),
    error: types.string.optional(),
    }))
    .states({
    idle: { loading: {} },
    loading: {
    success: { count: { gt: 0 } },
    error: { error: { notEq: "" } },
    },
    success: { idle: {} },
    error: { idle: {} },
    })
    .core()
    .processes((process) => ({
    loading: process()
    .action(async ({ context }) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    if (Math.random() > 0.8) {
    throw new Error("Случайная ошибка")
    }
    return { count: context.count + 1 }
    })
    .success(({ update, data }) => {
    update({ count: data.count, isLoading: false, error: "" })
    })
    .error(({ update, error }) => {
    update({ error: error.message, isLoading: false })
    }),
    }))
    .reactions()
    .view({
    render: ({ context, html, update }) => html`
    <div class="counter">
    <h2>Счётчик: ${context.count}</h2>
    <button @click=${() => update({ isLoading: true })} ?disabled=${context.isLoading}>
    ${context.isLoading ? "Загрузка..." : "Увеличить"}
    </button>
    ${context.error ? html`<div class="error">${context.error}</div>` : null}
    </div>
    `,
    style: ({ css }) => css`
    .counter {
    text-align: center;
    padding: 20px;
    }
    .error {
    color: red;
    margin-top: 10px;
    }
    button:disabled {
    opacity: 0.5;
    }
    `,
    })
    MetaFor("user-form")
    .context((types) => ({
    name: types.string.required(""),
    email: types.string.required(""),
    age: types.number.required(0),
    errors: types.array.required([]),
    isSubmitting: types.boolean.required(false),
    }))
    .states({
    editing: {
    submitting: {
    name: { length: { min: 2 } },
    email: { pattern: /@/ },
    age: { gte: 18 },
    },
    },
    submitting: {
    success: { isSubmitting: false },
    error: { errors: { length: { gt: 0 } } },
    },
    success: { editing: {} },
    error: { editing: {} },
    })
    .core()
    .processes((process) => ({
    submitting: process()
    .action(async ({ context }) => {
    const errors = []

    if (context.name.length < 2) {
    errors.push("Имя должно содержать минимум 2 символа")
    }

    if (!context.email.includes("@")) {
    errors.push("Некорректный email")
    }

    if (context.age < 18) {
    errors.push("Возраст должен быть не менее 18 лет")
    }

    if (errors.length > 0) {
    throw new Error(errors.join(", "))
    }

    // Имитация отправки на сервер
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return { success: true }
    })
    .success(({ update }) => {
    update({
    isSubmitting: false,
    errors: [],
    name: "",
    email: "",
    age: 0,
    })
    })
    .error(({ update, error }) => {
    update({
    isSubmitting: false,
    errors: error.message.split(", "),
    })
    }),
    }))
    .reactions()
    .view({
    render: ({ context, html, update }) => html`
    <form
    @submit=${(e) => {
    e.preventDefault()
    update({ isSubmitting: true })
    }}>
    <div>
    <label>Имя:</label>
    <input .value=${context.name} @input=${(e) => update({ name: e.target.value })} placeholder="Введите имя" />
    </div>

    <div>
    <label>Email:</label>
    <input
    .value=${context.email}
    @input=${(e) => update({ email: e.target.value })}
    placeholder="Введите email"
    type="email" />
    </div>

    <div>
    <label>Возраст:</label>
    <input
    .value=${context.age}
    @input=${(e) => update({ age: parseInt(e.target.value) || 0 })}
    type="number"
    min="0" />
    </div>

    <button type="submit" ?disabled=${context.isSubmitting}>
    ${context.isSubmitting ? "Отправка..." : "Отправить"}
    </button>

    ${context.errors.length > 0
    ? html` <div class="errors">${context.errors.map((error) => html`<div class="error">${error}</div>`)}</div> `
    : null}
    </form>
    `,
    style: ({ css }) => css`
    form {
    max-width: 400px;
    margin: 0 auto;
    padding: 20px;
    }

    div {
    margin-bottom: 15px;
    }

    label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
    }

    input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    }

    button {
    width: 100%;
    padding: 10px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    }

    button:disabled {
    background: #ccc;
    cursor: not-allowed;
    }

    .errors {
    margin-top: 15px;
    }

    .error {
    color: red;
    margin-bottom: 5px;
    }
    `,
    })
    // Родительский компонент с динамическим обновлением
    MetaFor("parent-dashboard")
    .context((types) => ({
    userMessage: types.string.required("Привет от родителя"),
    userCount: types.number.required(0),
    isLoading: types.boolean.required(false),
    }))
    .states({
    idle: { loading: {} },
    loading: { idle: {} },
    })
    .core()
    .processes((process) => ({
    loading: process()
    .action(async ({ context }) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return {
    userMessage: "Обновленное сообщение от родителя",
    userCount: context.userCount + 1,
    }
    })
    .success(({ update, data }) => {
    update({
    userMessage: data.userMessage,
    userCount: data.userCount,
    isLoading: false,
    })
    }),
    }))
    .reactions()
    .view({
    render: ({ context, html, update }) => html`
    <div class="dashboard">
    <h1>Родительский компонент</h1>
    <p>Сообщение: ${context.userMessage}</p>
    <p>Счетчик: ${context.userCount}</p>

    <button @click=${() => update({ isLoading: true })} ?disabled=${context.isLoading}>
    ${context.isLoading ? "Обновление..." : "Обновить данные"}
    </button>

    <metafor-child-widget
    context=${{
    message: context.userMessage,
    count: context.userCount,
    }}></metafor-child-widget>
    </div>
    `,
    style: ({ css }) => css`
    .dashboard {
    padding: 20px;
    border: 2px solid #007bff;
    border-radius: 8px;
    margin: 20px;
    }

    button {
    padding: 10px 20px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin: 10px 0;
    }

    button:disabled {
    background: #ccc;
    cursor: not-allowed;
    }
    `,
    })

    // Дочерний компонент, получающий контекст
    MetaFor("child-widget")
    .context((types) => ({
    message: types.string.required("Сообщение по умолчанию"),
    count: types.number.required(0),
    }))
    .states({ idle: {} })
    .core()
    .processes()
    .reactions()
    .view({
    render: ({ context, html }) => html`
    <div class="widget">
    <h3>Дочерний виджет</h3>
    <p>Полученное сообщение: ${context.message}</p>
    <p>Полученный счетчик: ${context.count}</p>
    <div class="status">Статус: ${context.count > 0 ? "Активен" : "Неактивен"}</div>
    </div>
    `,
    style: ({ css }) => css`
    .widget {
    padding: 15px;
    border: 1px solid #28a745;
    border-radius: 6px;
    margin-top: 15px;
    background: #f8f9fa;
    }

    .status {
    margin-top: 10px;
    padding: 5px 10px;
    background: #28a745;
    color: white;
    border-radius: 4px;
    text-align: center;
    }
    `,
    })

    MetaFor предоставляет встроенные инструменты отладки:

    // Включение отладки
    import { enableMetaForDebug } from "@zavx0z/metafor/debug/config"

    enableMetaForDebug()

    // Получение снапшота состояния
    const element = document.querySelector("metafor-my-component")
    const snapshot = element.getSnapshot()
    console.log(snapshot)