Skip to content

Repository files navigation

SmartObject

Умный объект с отслеживанием изменений, подписками на события и управлением состояниями. Идеален для форм, настроек и любых данных, требующих контроля изменений.

Возможности

  • Отслеживание изменений полей объекта в реальном времени.
  • Подписка на изменения конкретных полей или всего объекта.
  • Коммит (фиксация) текущих значений как «эталонных».
  • Откат к последним закоммиченным или изначальным значениям.
  • Проверка статуса изменений для каждого поля.
  • Сериализация в JSON с поддержкой вложенных SmartObject и дат (Date → ISO‑строка).
  • Рекурсивная обработка вложенных умных объектов.
  • Оптимизация уведомлений: уведомления отправляются только при фактическом изменении значения.

Установка

npm install smart-object-kit

# Или через yarn
yarn add smart-object-kit

Быстрый старт

import { SmartObject } from 'smart-object-kit'

const user = new SmartObject({
  name: 'ilya',
  firstname: 'burcev',
  age: 24,
})

user.setValue('age', 25)
console.log(user.isChanged()) // { age: true }

user.rejectChanges()
console.log(user.getValue('age')) // 24
console.log(user.isChanged())    // false

API

constructor(initialValues)

Создаёт новый умный объект. Начальные значения глубоко копируются и сохраняются как эталонные.

const settings = new SmartObject({ theme: 'dark', language: 'ru' })

getValue(key)

Возвращает текущее значение поля.

const name = user.getValue('name') // 'ilya'

setValue(key, value)

Задаёт полю новое значение. Уведомления отправляются только если значение действительно изменилось.

user.setValue('age', 25)  // уведомит подписчиков
user.setValue('age', 25)  // НЕ уведомит — значение не изменилось

subscribe(keys?, callback?)

Подписка на изменения. Возвращает функцию отписки.

// Подписка на конкретные поля
const unsub = user.subscribe(['name', 'age'], () => {
  console.log('name или age изменились')
})

// Подписка на все изменения
const unsubAll = user.subscribe(undefined, () => {
  console.log('Что-то изменилось')
})

// Отписка
unsub()

unsubscribe(keys?, callback?)

Удаляет подписчиков.

// Удалить конкретный колбэк с конкретного поля
user.unsubscribe(['name'], myCallback)

// Удалить всех подписчиков поля 'age'
user.unsubscribe(['age'])

// Удалить вообще всех подписчиков
user.unsubscribe()

commitChanges(keys?)

Фиксирует текущие значения как «эталонные». После коммита откат вернёт к этим значениям.

user.setValue('name', 'Ilya')
user.commitChanges()         // закоммитить все поля
// или
user.commitChanges(['name']) // закоммитить только 'name'

rejectChanges(keys?)

Откатывает значения к последним закоммиченным (или изначальным). Вызывает уведомления для откатанных полей.

user.setValue('age', 99)
user.rejectChanges(['age'])  // откатить только 'age'
// или
user.rejectChanges()         // откатить всё

isChanged()

Возвращает объект с изменёнными полями ({ fieldName: true }) или false, если ничего не изменено.

user.setValue('name', 'Alex')
console.log(user.isChanged()) // { name: true }

user.commitChanges()
console.log(user.isChanged()) // false

toJSONFormat()

Возвращает JSON-совместимый объект. Вложенные SmartObject рекурсивно разворачиваются, Date → ISO-строка.

const obj = user.toJSONFormat()
// { name: 'ilya', firstname: 'burcev', age: 24, profession: { name: 'frontend developer', ... } }

toJSONString()

Сериализует объект в JSON-строку.

const json = user.toJSONString()
// '{"name":"ilya","firstname":"burcev","age":24,"profession":{"name":"frontend developer",...}}'

Примеры

Вложенный SmartObject

Вложенные SmartObject автоматически учитываются при проверке изменений и сериализации.

const user = new SmartObject({
  name: 'ilya',
  profession: new SmartObject({
    name: 'frontend developer',
    experience: 5,
    salary: 2000,
  }),
})

// Изменение вложенного объекта
user.getValue('profession').setValue('salary', 3000)

console.log(user.getValue('profession').isChanged()) // { salary: true }
console.log(user.isChanged())                        // { profession: true }

Массив SmartObject

SmartObject внутри массивов также корректно отслеживаются.

const board = new SmartObject({
  columns: [
    new SmartObject({ title: 'To Do', color: '#ccc' }),
    new SmartObject({ title: 'In Progress', color: '#ff0' }),
    new SmartObject({ title: 'Done', color: '#0f0' }),
  ],
})

const columns = board.getValue('columns')
columns[0].setValue('color', '#999')

console.log(columns[0].isChanged()) // { color: true }
console.log(board.isChanged())      // { columns: true }

Работа с Date

Объекты Date автоматически преобразуются в ISO-строку при сериализации.

const event = new SmartObject({
  title: 'Конференция',
  date: new Date('2026-07-22'),
})

console.log(event.toJSONFormat())
// { title: 'Конференция', date: '2026-07-22T00:00:00.000Z' }

console.log(event.toJSONString())
// '{"title":"Конференция","date":"2026-07-22T00:00:00.000Z"}'

Типизация

SmartObject полностью типизирован — ключи и значения выводятся из начального объекта.

interface UserSchema {
  name: string
  age: number
  active: boolean
}

const user = new SmartObject<UserSchema>({
  name: 'Ilya',
  age: 24,
  active: true,
})

user.setValue('name', 'Alex')  // OK
user.setValue('age', 'test')   // Ошибка типа: string не назначаем number
user.getValue('name')          // тип: string
user.getValue('age')           // тип: number

Форма с откатом

const form = new SmartObject({
  email: 'user@mail.com',
  password: '',
  remember: false,
})

form.setValue('password', '123456')
form.setValue('remember', true)

if (form.isChanged()) {
  console.log('Форма изменена')
}

// Пользователь нажал «Отмена»
form.rejectChanges()

console.log(form.getValue('password')) // ''
console.log(form.getValue('remember')) // false

// Пользователь нажал «Сохранить»
form.setValue('password', 'newpass')
form.commitChanges() // фиксировали — теперь откат вернёт к этим значениям

Множественные подписки

const user = new SmartObject({ name: 'Ilya', age: 24 })

const onNameChange = user.subscribe(['name'], () => {
  console.log('Имя:', user.getValue('name'))
})

const onAnyChange = user.subscribe(undefined, () => {
  console.log('Что-то изменилось')
})

user.setValue('name', 'Alex')
// → "Имя: Alex"
// → "Что-то изменилось"

user.setValue('age', 25)
// → "Что-то изменилось"   (подписчик 'name' не вызывается)

onNameChange()

user.setValue('name', 'Oleg')
// → "Что-то изменилось"   (только глобальный подписчик)

Использование в React

Хук useSmartObject

Базовый хук для реактивной привязки SmartObject к компоненту. Перерисовка происходит только при изменении указанных полей.

import { useState, useEffect } from 'react'
import { SmartObject } from 'smart-object-kit'

function useSmartObject<T extends Record<string, any>, K extends keyof T>(
  smartObj: SmartObject<T>,
  keys: K[]
): Pick<T, K> {
  const [, setTick] = useState(0)

  useEffect(() => {
    const unsubscribe = smartObj.subscribe(keys, () => {
      setTick(tick => tick + 1)
    })
    return () => unsubscribe()
  }, [smartObj, keys.join(',')])

  const result = {} as Pick<T, K>
  keys.forEach(key => {
    result[key] = smartObj.getValue(key)
  })
  return result
}

Компонент формы с Save/Undo

interface UserSchema {
  name: string
  age: number
  email: string
}

const userStore = new SmartObject<UserSchema>({
  name: 'Ivan',
  age: 25,
  email: 'ivan@mail.com',
})

function UserForm() {
  const { name, age, email } = useSmartObject(userStore, ['name', 'age', 'email'])
  const changes = userStore.isChanged()

  const handleChange = (field: keyof UserSchema, value: string) => {
    if (field === 'age') {
      userStore.setValue(field, Number(value))
    } else {
      userStore.setValue(field, value)
    }
  }

  return (
    <div>
      <input
        value={name}
        onChange={e => handleChange('name', e.target.value)}
      />
      <input
        value={String(age)}
        onChange={e => handleChange('age', e.target.value)}
      />
      <input
        value={email}
        onChange={e => handleChange('email', e.target.value)}
      />

      {changes && <span style={{ color: 'orange' }}>Есть несохранённые изменения</span>}

      <button onClick={() => userStore.commitChanges()}>Сохранить</button>
      <button onClick={() => userStore.rejectChanges()} disabled={!changes}>
        Отменить
      </button>
    </div>
  )
}

Выборочная подписка — оптимизация рендеров

Каждый компонент подписывается только на нужные поля, что предотвращает лишние перерисовки.

const appStore = new SmartObject({
  theme: 'dark',
  language: 'ru',
  userName: 'Ilya',
  notifications: 3,
})

function ThemeToggle() {
  const { theme } = useSmartObject(appStore, ['theme'])

  return (
    <button onClick={() => appStore.setValue('theme', theme === 'dark' ? 'light' : 'dark')}>
      Тема: {theme}
    </button>
  )
}

function NotificationBadge() {
  const { notifications } = useSmartObject(appStore, ['notifications'])

  return <span>Уведомления: {notifications}</span>
}

function UserProfile() {
  const { userName } = useSmartObject(appStore, ['userName'])

  return <p>Пользователь: {userName}</p>
}

// Изменение theme НЕ вызывает перерисовку NotificationBadge и UserProfile
appStore.setValue('theme', 'light')

Глобальный стор с вложенным SmartObject

const userStore = new SmartObject({
  name: 'Ilya',
  settings: new SmartObject({
    theme: 'dark',
    language: 'ru',
  }),
})

function SettingsPanel() {
  const settings = userStore.getValue('settings')
  const { theme } = useSmartObject(settings, ['theme'])

  return (
    <div>
      <p>Тема: {theme}</p>
      <button onClick={() => settings.setValue('theme', theme === 'dark' ? 'light' : 'dark')}>
        Переключить
      </button>
    </div>
  )
}

Custom Hook с логикой отката

function useFormWithUndo<T extends Record<string, any>>(
  store: SmartObject<T>,
  keys: (keyof T)[]
) {
  const values = useSmartObject(store, keys)
  const changes = store.isChanged()

  const save = () => store.commitChanges()
  const undo = () => store.rejectChanges()

  return { values, changes, save, undo }
}

// Использование
function ProfileEditor() {
  const store = new SmartObject({ name: 'Ilya', bio: 'Developer' })
  const { values, changes, save, undo } = useFormWithUndo(store, ['name', 'bio'])

  return (
    <div>
      <input
        value={values.name}
        onChange={e => store.setValue('name', e.target.value)}
      />
      <textarea
        value={values.bio}
        onChange={e => store.setValue('bio', e.target.value)}
      />

      {changes && <p style={{ color: 'orange' }}>Не сохранено</p>}

      <button onClick={save}>Сохранить</button>
      <button onClick={undo} disabled={!changes}>Отменить</button>
    </div>
  )
}

Лицензия

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages