You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
А дальше я расскажу, как сотворить такое же за вечер используя фреймворк $mol...
Это что за покемон?
$mol - современный фреймворк для быстрого создания кроссплатформенных отзывчивых веб-приложений. Он базируется на архитектуре MAM устанавливающей следующие правила для всех модулей:
Модуль - это директория, содержащая исходные коды.
Исходные коды могут быть на самых разных языках.
Все языки равноправны в рамках модуля.
Модули могут образовывать иерархию.
Имя модуля жёстко соответствует пути к нему в файловой системе.
Между модулями могут быть зависимости.
Информация о зависимостях модуля получается статическим анализом его исходных кодов.
Любой модуль можно собрать как набор независимых бандлов на разных языках (js, css, tree...).
В бандлы попадают только те модули, что реально используются.
В бандл попадают все исходные коды модуля.
У модулей нет версий - всегда используется актуальный код.
Интерфейс модулей должен быть открыт для расширения, но закрыт для изменения.
Если нужен другой интерфейс - нужно создать новый модуль. Например /my/file/ и /my/file2/. Это позволит использовать оба интерфейса не путаясь в них.
Рабочее окружение
Начать разработку на $mol очень просто. Вы один раз разворачиваете рабочее окружение и далее клепаете приложения/библиотеки как пирожки.
Если вы работаете под Windows, то стоит настроить GIT, чтобы он не менял концы строк в ваших исходниках:
git config --global core.autocrlf input
Теперь следует развернуть MAM проект, который автоматически поднимет вам девелоперский сервер:
git clone https://github.com/eigenmethod/mam.git
cd mam
npm install
npm start
Всё, сервер разработчика запущен, можно открывать редактор. Обратите внимание, что в редакторе нужно открывать именно директорию MAM проекта, а не проекта конкретного приложения или вашей компании.
Как видите, начать разрабатывать на $mol очень просто. Основной принцип MAM архитектуры - из коробки всё должно работать как следует, а не требовать долгой утомительной настройки.
Каркас приложения
Для конспирации наше приложение будет иметь позывной $mol_app_calc. По правилам MAM лежать оно должно соответственно в директории /mol/app/calc/. Все файлы в дальнейшем мы будем создавать именно там.
Первым делом создадим точку входа - простой index.html:
Ничего особенного, разве что мы указали точку монтирования приложения специальным атрибутом mol_view_root в котором обозначили, что монтировать надо именно наше приложение. Архитектура $mol такова, что любой компонент может выступать в качестве корня приложения. И наоборот, любое $mol приложение - не более, чем обычный компонент и может быть легко использовано внутри другого приложения. Например, в галерее приложений.
Обратите внимание, что мы уже сразу прописали пути к скриптам и стилям - эти бандлы будут собираться автоматически для нашего приложения и включать в себя только те исходные коды, что реально ему необходимы. Забегая вперёд стоит заметить, что общий объём приложения составит каких-то 36KB без минификации, но с зипованием:
Итак, чтобы объявить компонент, который будет нашим приложением, нам нужно создать файл calc.view.tree, простейшее содержимое которого состоит всего из одной строчки:
$mol_app_calc $mol_page
Второе слово - имя базового компонента, а первое - имя нашего, который будет унаследован от базового. Таким образом каждый компонент является преемником какого-либо другого. Самый-самый базовый компонент, от которого происходят все остальные - $mol_view. Он даёт всем компонентам лишь самые базовые стили и поведение. В нашем случае, базовым будет компонент $mol_page представляющий собой страницу с шапкой, телом и подвалом.
Из calc.view.tree будет автоматически сгенерирован TypeScript класс компонента и помещён в -view.tree/calc.view.tree.ts, чтобы среда разработки могла его подхватить:
Собственно, сейчас приложение уже можно открыть по адресу http://localhost:8080/mol/app/calc/ и увидеть пустую страничку c позывным в качестве заголовка:
Синтаксис view.tree довольно необычен, но он прост и лаконичен. Позволю себе процитировать один из отзывов о нём:
Синтаксис tree очень легко читать, но нужно немного привыкнуть и не бросить всё раньше времени 😜. Мой мозг переваривал и негодовал около недели, а потом приходит просветление и понимаешь как сильно этот фреймворк упрощает процесс разработки. (c) Виталий Макеев
Так что не пугаемся, а погружаемся! И начнём с общей раскладки страницы - она будет состоять у нас из шапки, панели редактирования текущей ячейки и собственно таблицы с данными.
У каждого компонента есть свойство sub(), которое возвращает список того, что должно быть отрендерено непосредственно внутри компонента. У $mol_page туда рендерятся значения свойств Head(), Body() и Foot(), которые возвращают соответствующе подкомпоненты:
$mol_page $mol_view
sub /
<= Head $mol_view
<= Body $mol_scroll
<= Foot $mol_view
В данном коде опущены детали реализации подкомпонент, чтобы была видна суть. Объявляя подкомпонент (он же "Элемент" в терминологии БЭМ) мы указываем его имя в контексте нашего компонента и имя класса, который должен быть инстанцирован. Созданный таким образом экземпляр компонента будет закеширован и доступен через одноимённое свойство. Например, this.Body() в контексте нашего приложения вернёт настроенный экземпляр $mol_scroll. Говоря паттернами, свойство Body() выступает в качестве локальной ленивой фабрики.
Давайте преопределим свойство sub(), чтобы оно возвращало нужные нам компоненты:
$mol_app_calc $mol_page
sub /
<= Head -
<= Current $mol_bar
<= Body $mol_grid
Тут мы оставили шапку от $mol_page, добавили $mol_bar в качестве панельки редактирования текущей ячейки, в качестве тела страницы использовали $mol_grid - компонент для рисования виртуальных таблиц, а подвал так и вовсе убрали, так как он нам без надобности.
Давайте взглянем, как изменился сгенерированный класс:
namespace${exportclass$mol_app_calcextends$mol_page{/// sub / /// <= Head - /// <= Current - /// <= Body -sub(){return[].concat(this.Head(),this.Current(),this.Body())}/// Current $mol_bar
@ $mol_memCurrent(){returnnewthis.$.$mol_bar}/// Body $mol_grid
@ $mol_memBody(){returnnewthis.$.$mol_grid}}}
Визитная карточка $mol - очень "читабельный" код. Это касается не только генерируемого кода, но и кода модулей самого $mol, и прикладного кода создаваемых на его базе приложений.
Возможно вы обратили внимание на то, что объекты создаются не прямым инстанцированием по имени класса new $mol_grid, а через this.$. Поле $ есть у любого компонента и возвращает глобальный контекст или реестр, говоря паттернами. Отличительной особенностью доступа ко глобальным значениям через поле $ является возможность любому компоненту переопределить контекст для всех вложенных в него на любую глубину компонентов. Таким образом $mol в крайне практичной и ненавязчивой форме реализует инверсию контроля, позволяющую подменять реализации использующиеся где-то в глубине переиспользуемого компонента.
Формирование таблицы
Что ж, давайте нарастим немного мясца и настроим вложенные компоненты под себя: гриду нужно объяснить, какие у нас будут идентификаторы столбцов, какие идентификаторы строк, а также списки ячеек в шапке и теле таблицы.
Как видите, мы просто переопределили соответствующие свойства вложенного компонента на свои реализации. Это очень простая, но в то же время мощная техника, позволяющая реактивно связывать компоненты друг с другом. В синтаксисе view.tree поддерживается 3 типа связывания:
Левостороннее (как в коде выше), когда мы указываем вложенному компоненту какое значение должно возвращать его свойство.
Правостороннее, когда мы создаём у себя свойство, которое выступает алиасом для свойства вложенного компонента.
Двустороннее, когда указываем вложенному компоненту читать из и писать в наше свойство, думая, что работает со своим.
Для иллюстрации двустороннего связывания, давайте детализируем панель редактирования текущей ячейки:
Current $mol_bar
sub /
<= Pos $mol_string
enabled false
value <= pos \
<= Edit $mol_string
hint \=
value?val <=> formula_current?val \
Как видно оно у нас будет состоять у нас из двух полей ввода:
Координаты ячейки. Пока что запретим их изменять через свойство enabled - оставим этот функционал на будущее.
Поле ввода формулы. Тут мы уже двусторонне связываем свойство value поля ввода и наше свойство formula_current, которое мы тут же и объявляем, указав значение по умолчанию - пустую строку.
Код свойств Edit и formula_current будет сгенерирован примерно следующий:
Пока что у нас было лишь декларативное описание композиции компонент. Прежде чем мы начнём описывать логику работы, давайте сразу объявим как у нас будут выглядеть ячейки:
Заголовки строк и колонок у нас будут плавающими, поэтому мы используем для них компонент $mol_float, который отслеживает позицию скроллинга, предоставляемую компонентом $mol_scroll через контекст, и смещает компонент так, чтобы он всегда был в видимой области. А для ячейки заводим отдельный компонент $mol_app_calc_cell:
Этот компонент у нас будет кликабельным, поэтому мы наследуем его от $mol_button. События кликов мы направляем в свойство select, которое в дальнейшем у нас будет переключать редактор ячейки на ту, по которой кликнули. Кроме того, мы добавляем сюда пару атрибутов, чтобы по особенному стилизовать выбранную ячейку и обеспечить ячейкам числового типа выравниванием по правому краю. Забегая верёд, стили для ячеек у нас будут простые:
[mol_app_calc_cell] {
user-select: text; /* по умолчанию $mol_button не выделяемый */background:var(--mol_skin_card); /* используем css-variables благодаря post-css */
}
[mol_app_calc_cell_selected] {
box-shadow:var(--mol_skin_focus_outline);
z-index:1;
}
[mol_app_calc_cell_type="number"] {
text-align: right;
}
Обратите внимание на одноимённый компоненту селектор [mol_app_calc_cell] - соответствующий атрибут добавляется dom-узлу автоматически, полностью избавляя программиста от ручной работы по расстановке css-классов. Это упрощает разработку и гарантирует консистентность именования.
Наконец, чтобы добавить свою логику, мы создаём calc.view.ts, где создаём класс в пространстве имён $.$$, который наследуем от одноимённого автоматически сгенерированного класса из пространства имён $:
Во время исполнения оба пространства имён будут указывать на один и тот же объект, а значит наш класс с логикой после того как отнаследуется от автогенерированного класса просто займёт его место. Благодаря такой хитрой манипуляции добавление класса с логикой остаётся опциональным, и применяется только, когда декларативного описания не хватает. Например, переопределим свойство select(), чтобы при попытке записать в него объект события, оно изменяло свойство selected() на true:
Они зависят от свойства dimensions(), которое мы будем вычислять на основе заполненности ячеек, так, чтобы у любой заполненной ячейки было ещё минимум две пустые справа и снизу:
Как видно, свойство formulas() изменяемое, то есть мы можем через него как прочитать формулы для ячеек, так и записать обновление в адресную строку. Например, если выполнить: this.formulas({ 'B1' : '24' }), то в адресной строке мы увидим уже #A1=12/B1=24.
Адресная строка
Кроссплатформенный модуль $mol_state_arg позволяет нам работать с параметрами приложения как со словарём, но как правило удобнее получать и записывать конкретный параметр по имени. Например, позволим пользователю изменять название нашей таблицы, которое мы опять же будем сохранять в адресной строке:
title( next? : string ) {
const title = this.$.$mol_state_arg.value( `title` , next )
return title == undefined ? super.title() : title
}
Как можно заметить, если в адресной строке имя таблицы не задано, то будет взято имя заданное в родительском классе, который генерируется из calc.view.tree, который мы сейчас обновим, добавив в шапку вместо простого вывода заголовка, поле ввода-вывода заголовка:
head() - свойство из $mol_page, которое возвращает список того, что должно быть отрендерено внутри подкомпонента Head(). Это типичный паттерн в $mol - называть вложенный компонент и его содержимое одним и тем же словом, с той лишь разницей, что имя компонента пишется с большой буквы.
Tools() - панель инструментов из $mol_page, отображаемая с правой стороны шапки. Давайте сразу же заполним и её, поместив туда кнопку скачивания таблицы в виде CSV файла:
tools /
<= Download $mol_link
hint <= download_hint @ \Download
file_name <= download_file \
uri <= download_uri?val \
click?event <=> download_generate?event null
sub /
<= Download_icon $mol_icon_load
$mol_link - компонент для формирования ссылок. Если ему указать file_name(), то по клику он предложит скачать файл по ссылке, сохранив его под заданным именем. Давайте же сразу сформируем это имя на основе имени таблицы:
Обратите внимание на символ собачки перед значением по умолчанию на английском языке:
download_hint @ \Download
Вставка этого символа - это всё, что вам необходимо, чтобы добавить вашему приложению поддержку локализации. В сгенерированном классе не будет строки "Download" - там будет лишь запрос за локализованным текстом:
Как видно, для текстов были сформированы уникальные человекопонятные ключи. Вы можете отдать этот файл переводчикам и переводы от них поместить в фалы вида *.locale=*.json. Например, добавим нашему компоненту переводы на русский язык в файл calc.locale=ru.json:
Теперь, если у вас в браузере выставлен русский язык в качестве основного, то при старте приложения, будет асинхронно подгружен бандл с русскоязычными текстами -/web.locale=ru.json. А пока идёт загрузка, компоненты, зависящие от переводов, будут автоматически показывать индикатор загрузки.
Заполняем ячейки
Итак, у нас есть идентификаторы строк и столбцов. Давайте сформируем списки ячеек. Сперва заголовки колонок:
Тут мы с помощью $mol_defer ставим отложенную задачу перенести фокус на редактор всякий раз когда меняется идентификатор текущей ячейки. Отложенные задачи выполняются в том же фрейме анимации, а значит пользователь не увидит никакого мерцания от перефокусировки. Если бы мы перенесли фокус сразу, то подписались бы на состояние сфокусированности редактора и при перемещении фокуса - сбрасывался бы и идентификатор текущей ячейки, что нам, разумеется, не надо.
Клавиатурная навигация
Постоянно тыкать мышью в ячейки для перехода между ними не очень-то удобно. Стрелочками на клавиатуре было бы быстрее. Традиционно в электронных таблицах есть два режима: режим навигации и режим редактирования. Постоянно переключаться между ними тоже напрягает. Поэтому мы сделаем ход конём и совместим редактирование и навигацию. Фокус будет постоянно оставаться на панели редактирования ячейки, но при зажатой клавише Alt, нажатие стрелочек, будет изменять редактируемую ячейку на одну из соседних. Для подобных выкрутасов есть специальный компонент $mol_nav, который является компонентом-плагином.
В $mol есть 3 вида компонент:
Обычные компоненты, которые создают dom-узел и контролируют его состояние.
Призрачные компоненты, которые не создают dom-узлов, а используют dom-узел переданного им компонента, для добавления поведения/отображения.
Компоненты-плагины, которые тоже не создают dom-узлов, а используют dom-узел компонента владельца для добавления поведения/отображения.
Добавляются плагины через свойство plugins(). Например, добавим клавиатурную навигацию нашему приложению:
Тут мы указали, что навигироваться мы будем по горизонтали и по вертикали, по идентификаторам столбцов и колонок, соответственно. Текущие координаты мы будем синхронизировать со свойствами current_col() и current_row(), которые мы провяжем с собственно current():
Всё, теперь нажатие Alt+Right, например, будет делать редактируемой ячейку справа от текущей, и так пока не упрётся в самую правую ячейку.
Копирование и вставка
Так как ячейки у нас являются ни чем иным, как нативными td dom-элементами, то браузер нам здорово помогает с копированием. Для этого достаточно зажать ctrl, выделить ячейки и скопировать их в буфер обмена. Текстовое представление содержимого буфера будет ни чем иным, как Tab Separated Values, который легко распарсить при вставке. Так что мы смело добавляем обработчик соответствующего события:
Славно, что всё это работает не только в рамках нашего приложения - вы так же можете копипастить данные и между разными табличными процессорами, такими как Microsoft Excel или LibreOffice Calc.
Выгрузка файла
Частая хотелка - экспорт данных в файл. Кнопку мы уже добавили ранее. Осталось лишь реализовать формирование ссылки на экспорт. Ссылка должна быть data-uri вида data:text/csv;charset=utf-8,{'url-кодированный текст файла}. Содержимое CSV для совместимости с Microsoft Excel должно удовлетворять следующим требованиям:
После установки новой ссылки, мы форсируем запуск отложенных задач, чтобы произошёл рендеринг в dom-дерево до выхода из текущего обработчика событий. Нужно это для того, чтобы браузер подхватил свежесгенерированную ссылку, а не предлагал скачать предыдущую версию файла.
Формулы
Самое главное в электронных таблицах - не сами данные, а формулы, через которые можно связывать значения одних ячеек со значениями других. При этом за актуальностью вычисляемых значений электронная таблица следит сама, реактивно обновляя значения в ячейках зависимых от редактируемой в данный момент пользователем.
В нашем случае пользователь всегда редактирует именно формулу. Даже если просто вводит текст - это на самом деле формула, возвращающая этот текст. Но если он начнёт свой ввод с символа =, то сможет использовать внутри различные математические выражения и, в том числе, обращаться к значениям других ячеек.
Реализовывать парсинг и анализ выражений - довольно сложная задача, а вечеринке уже мерещится ДедЛайн, так что мы не долго думая воспользуемся всей мощью JavaScript и позволим пользователю писать любые JS выражения. Но, чтобы он случайно не отстрелил ногу ни себе, ни кому-то ещё, будем исполнять его выражение в песочнице $mol_func_sandbox, которая ограничит мощь JavaScript до разрешённых нами возможностей:
Как видите, мы разрешили пользователю использовать математические функции и константы, а также предоставили пару функций: для получения формулы ячейки и вычисленного значения ячейки по её идентификатору.
Песочница позволяет нам преобразовывать исходный код выражения в безопасные функции, которые можно безбоязненно вызывать.
Заставлять пользователя писать вызов функции result вручную - слишком жестоко. Поэтому мы слегка изменяем введённую формулу, находя комбинации символов, похожие на кодовые имена ячеек вида AB34, и заменяя их на вызовы result. Дополнительно, вместо значения, можно будет получить формулу из ячейки, приписав спереди собачку: @AB34. Создание таких функций - не бесплатно, так что если в ячейке у нас просто текст, а не выражение, то мы так его и возвращаем безо всяких песочниц.
Осталось дело за малым - реализовать свойство result() с дополнительной постобработкой для гибкости:
@ $mol_mem_key
result( id : { row : number , col : string } ) {
const res = this.func( id ).call()
if( res === undefined ) return ''
if( res === '' ) return ''
if( isNaN( res ) ) return res
return Number( res )
}
Тут мы избавились от возможного значения undefined, а так же добавили преобразование строк похожих на числа в собственно числа.
Финальный аккорд
На этом основная программа нашей вечеринки подходит к концу. Полный код приложения $mol_app_calc доступен на ГитХабе. Но прошу вас не спешить расходиться. Давайте каждый возьмёт по электронной таблице в свои руки и попробует сделать с ней что-нибудь эдакое. Вместе у нас может получиться интересная галерея примеров её использования. Итак...
The text was updated successfully, but these errors were encountered:
https://page.hyoo.ru/#!=6vhts9_pxb81u
Здравствуйте, меня зовут Дмитрий Карловский и я.. обожаю математику. Однажды мне не спалось и я запилил сервис для таких же отбитых как и я - легковесную электронную таблицу с пользовательскими формулами, шарингом и скачиванием.
Живой пример с расчётом кредита:
А дальше я расскажу, как сотворить такое же за вечер используя фреймворк $mol...
Это что за покемон?
$mol - современный фреймворк для быстрого создания кроссплатформенных отзывчивых веб-приложений. Он базируется на архитектуре MAM устанавливающей следующие правила для всех модулей:
/my/file/
и/my/file2/
. Это позволит использовать оба интерфейса не путаясь в них.Рабочее окружение
Начать разработку на $mol очень просто. Вы один раз разворачиваете рабочее окружение и далее клепаете приложения/библиотеки как пирожки.
Для начала вам потребуется установить:
Если вы работаете под Windows, то стоит настроить GIT, чтобы он не менял концы строк в ваших исходниках:
Теперь следует развернуть MAM проект, который автоматически поднимет вам девелоперский сервер:
Всё, сервер разработчика запущен, можно открывать редактор. Обратите внимание, что в редакторе нужно открывать именно директорию MAM проекта, а не проекта конкретного приложения или вашей компании.
Как видите, начать разрабатывать на $mol очень просто. Основной принцип MAM архитектуры - из коробки всё должно работать как следует, а не требовать долгой утомительной настройки.
Каркас приложения
Для конспирации наше приложение будет иметь позывной
$mol_app_calc
. По правилам MAM лежать оно должно соответственно в директории/mol/app/calc/
. Все файлы в дальнейшем мы будем создавать именно там.Первым делом создадим точку входа - простой
index.html
:Ничего особенного, разве что мы указали точку монтирования приложения специальным атрибутом
mol_view_root
в котором обозначили, что монтировать надо именно наше приложение. Архитектура $mol такова, что любой компонент может выступать в качестве корня приложения. И наоборот, любое $mol приложение - не более, чем обычный компонент и может быть легко использовано внутри другого приложения. Например, в галерее приложений.Обратите внимание, что мы уже сразу прописали пути к скриптам и стилям - эти бандлы будут собираться автоматически для нашего приложения и включать в себя только те исходные коды, что реально ему необходимы. Забегая вперёд стоит заметить, что общий объём приложения составит каких-то 36KB без минификации, но с зипованием:
Итак, чтобы объявить компонент, который будет нашим приложением, нам нужно создать файл
calc.view.tree
, простейшее содержимое которого состоит всего из одной строчки:Второе слово - имя базового компонента, а первое - имя нашего, который будет унаследован от базового. Таким образом каждый компонент является преемником какого-либо другого. Самый-самый базовый компонент, от которого происходят все остальные - $mol_view. Он даёт всем компонентам лишь самые базовые стили и поведение. В нашем случае, базовым будет компонент $mol_page представляющий собой страницу с шапкой, телом и подвалом.
Из
calc.view.tree
будет автоматически сгенерирован TypeScript класс компонента и помещён в-view.tree/calc.view.tree.ts
, чтобы среда разработки могла его подхватить:Собственно, сейчас приложение уже можно открыть по адресу
http://localhost:8080/mol/app/calc/
и увидеть пустую страничку c позывным в качестве заголовка:Синтаксис view.tree довольно необычен, но он прост и лаконичен. Позволю себе процитировать один из отзывов о нём:
Так что не пугаемся, а погружаемся! И начнём с общей раскладки страницы - она будет состоять у нас из шапки, панели редактирования текущей ячейки и собственно таблицы с данными.
У каждого компонента есть свойство
sub()
, которое возвращает список того, что должно быть отрендерено непосредственно внутри компонента. У $mol_page туда рендерятся значения свойствHead()
,Body()
иFoot()
, которые возвращают соответствующе подкомпоненты:В данном коде опущены детали реализации подкомпонент, чтобы была видна суть. Объявляя подкомпонент (он же "Элемент" в терминологии БЭМ) мы указываем его имя в контексте нашего компонента и имя класса, который должен быть инстанцирован. Созданный таким образом экземпляр компонента будет закеширован и доступен через одноимённое свойство. Например,
this.Body()
в контексте нашего приложения вернёт настроенный экземпляр $mol_scroll. Говоря паттернами, свойствоBody()
выступает в качестве локальной ленивой фабрики.Давайте преопределим свойство
sub()
, чтобы оно возвращало нужные нам компоненты:Тут мы оставили шапку от $mol_page, добавили $mol_bar в качестве панельки редактирования текущей ячейки, в качестве тела страницы использовали $mol_grid - компонент для рисования виртуальных таблиц, а подвал так и вовсе убрали, так как он нам без надобности.
Давайте взглянем, как изменился сгенерированный класс:
Визитная карточка $mol - очень "читабельный" код. Это касается не только генерируемого кода, но и кода модулей самого $mol, и прикладного кода создаваемых на его базе приложений.
Возможно вы обратили внимание на то, что объекты создаются не прямым инстанцированием по имени класса
new $mol_grid
, а черезthis.$
. Поле$
есть у любого компонента и возвращает глобальный контекст или реестр, говоря паттернами. Отличительной особенностью доступа ко глобальным значениям через поле$
является возможность любому компоненту переопределить контекст для всех вложенных в него на любую глубину компонентов. Таким образом $mol в крайне практичной и ненавязчивой форме реализует инверсию контроля, позволяющую подменять реализации использующиеся где-то в глубине переиспользуемого компонента.Формирование таблицы
Что ж, давайте нарастим немного мясца и настроим вложенные компоненты под себя: гриду нужно объяснить, какие у нас будут идентификаторы столбцов, какие идентификаторы строк, а также списки ячеек в шапке и теле таблицы.
Генерируемый класс расширится следующим описанием:
Как видите, мы просто переопределили соответствующие свойства вложенного компонента на свои реализации. Это очень простая, но в то же время мощная техника, позволяющая реактивно связывать компоненты друг с другом. В синтаксисе
view.tree
поддерживается 3 типа связывания:Для иллюстрации двустороннего связывания, давайте детализируем панель редактирования текущей ячейки:
Как видно оно у нас будет состоять у нас из двух полей ввода:
enabled
- оставим этот функционал на будущее.value
поля ввода и наше свойствоformula_current
, которое мы тут же и объявляем, указав значение по умолчанию - пустую строку.Код свойств
Edit
иformula_current
будет сгенерирован примерно следующий:Благодаря реактивному мемоизирующему декоратору $mol_mem, возвращаемое методом
formula_current
значение кешируется до тех пока пока оно кому-нибудь нужно.Пока что у нас было лишь декларативное описание композиции компонент. Прежде чем мы начнём описывать логику работы, давайте сразу объявим как у нас будут выглядеть ячейки:
Заголовки строк и колонок у нас будут плавающими, поэтому мы используем для них компонент $mol_float, который отслеживает позицию скроллинга, предоставляемую компонентом $mol_scroll через контекст, и смещает компонент так, чтобы он всегда был в видимой области. А для ячейки заводим отдельный компонент
$mol_app_calc_cell
:Этот компонент у нас будет кликабельным, поэтому мы наследуем его от $mol_button. События кликов мы направляем в свойство
select
, которое в дальнейшем у нас будет переключать редактор ячейки на ту, по которой кликнули. Кроме того, мы добавляем сюда пару атрибутов, чтобы по особенному стилизовать выбранную ячейку и обеспечить ячейкам числового типа выравниванием по правому краю. Забегая верёд, стили для ячеек у нас будут простые:Обратите внимание на одноимённый компоненту селектор
[mol_app_calc_cell]
- соответствующий атрибут добавляется dom-узлу автоматически, полностью избавляя программиста от ручной работы по расстановке css-классов. Это упрощает разработку и гарантирует консистентность именования.Наконец, чтобы добавить свою логику, мы создаём
calc.view.ts
, где создаём класс в пространстве имён$.$$
, который наследуем от одноимённого автоматически сгенерированного класса из пространства имён$
:Во время исполнения оба пространства имён будут указывать на один и тот же объект, а значит наш класс с логикой после того как отнаследуется от автогенерированного класса просто займёт его место. Благодаря такой хитрой манипуляции добавление класса с логикой остаётся опциональным, и применяется только, когда декларативного описания не хватает. Например, переопределим свойство
select()
, чтобы при попытке записать в него объект события, оно изменяло свойствоselected()
наtrue
:А свойство
type()
у нас будет возвращать тип ячейки, анализируя свойствоvalue()
:Но давайте вернёмся к таблице. Аналогичным образом мы добавляем логику к компоненту
$mol_app_calc
:Первым делом нам надо сформировать списки идентификаторов строк
row_ids()
и столбцовcol_ids()
:Они зависят от свойства
dimensions()
, которое мы будем вычислять на основе заполненности ячеек, так, чтобы у любой заполненной ячейки было ещё минимум две пустые справа и снизу:Методы
string2number()
иnumber2string()
просто преобразуют буквенные координаты колонок в числовые и наоборот:Размерность таблицы мы вычисляем на основе реестра формул, который берём из свойства
formulas()
. Возвращать оно должно json вида:А сами формулы мы будем брать и строки адреса, вида
#A1=12/B1=%3DA1*2
:Как видно, свойство
formulas()
изменяемое, то есть мы можем через него как прочитать формулы для ячеек, так и записать обновление в адресную строку. Например, если выполнить:this.formulas({ 'B1' : '24' })
, то в адресной строке мы увидим уже#A1=12/B1=24
.Адресная строка
Кроссплатформенный модуль $mol_state_arg позволяет нам работать с параметрами приложения как со словарём, но как правило удобнее получать и записывать конкретный параметр по имени. Например, позволим пользователю изменять название нашей таблицы, которое мы опять же будем сохранять в адресной строке:
Как можно заметить, если в адресной строке имя таблицы не задано, то будет взято имя заданное в родительском классе, который генерируется из
calc.view.tree
, который мы сейчас обновим, добавив в шапку вместо простого вывода заголовка, поле ввода-вывода заголовка:head()
- свойство из $mol_page, которое возвращает список того, что должно быть отрендерено внутри подкомпонентаHead()
. Это типичный паттерн в $mol - называть вложенный компонент и его содержимое одним и тем же словом, с той лишь разницей, что имя компонента пишется с большой буквы.Tools()
- панель инструментов из $mol_page, отображаемая с правой стороны шапки. Давайте сразу же заполним и её, поместив туда кнопку скачивания таблицы в виде CSV файла:$mol_link - компонент для формирования ссылок. Если ему указать
file_name()
, то по клику он предложит скачать файл по ссылке, сохранив его под заданным именем. Давайте же сразу сформируем это имя на основе имени таблицы:Локализация
Обратите внимание на символ собачки перед значением по умолчанию на английском языке:
Вставка этого символа - это всё, что вам необходимо, чтобы добавить вашему приложению поддержку локализации. В сгенерированном классе не будет строки "Download" - там будет лишь запрос за локализованным текстом:
А сами английские тексты будут автоматически вынесены в отдельный файл
-view.tree/calc.view.tree.locale=en.json
:Как видно, для текстов были сформированы уникальные человекопонятные ключи. Вы можете отдать этот файл переводчикам и переводы от них поместить в фалы вида
*.locale=*.json
. Например, добавим нашему компоненту переводы на русский язык в файлcalc.locale=ru.json
:Теперь, если у вас в браузере выставлен русский язык в качестве основного, то при старте приложения, будет асинхронно подгружен бандл с русскоязычными текстами
-/web.locale=ru.json
. А пока идёт загрузка, компоненты, зависящие от переводов, будут автоматически показывать индикатор загрузки.Заполняем ячейки
Итак, у нас есть идентификаторы строк и столбцов. Давайте сформируем списки ячеек. Сперва заголовки колонок:
Обратите внимание, мы добавили лишнюю колонку вначале, так как в ней у нас будут располагаться заголовки строк. А вот и ячейки для строк:
Далее, вспоминаем, про свойства, которые мы провязывали для ячеек:
У ячейки это просто обычные свойства, а у нас они принимают ключ - идентификатор ячейки.
Введём свойство
current()
которое будет хранить идентификатор текущей ячейки:А в реализации
selected()
мы просто будем сравнивать ячейку по переданному идентификатору и по текущему:Разумеется, если в
selected()
переданоtrue
, то будет установлен новый идентификатор в качестве текущего и сравнение ячеек тоже дастtrue
.Последний штрих - при выборе ячейки было бы не плохо переносить фокус с её самой на редактор значения:
Тут мы с помощью $mol_defer ставим отложенную задачу перенести фокус на редактор всякий раз когда меняется идентификатор текущей ячейки. Отложенные задачи выполняются в том же фрейме анимации, а значит пользователь не увидит никакого мерцания от перефокусировки. Если бы мы перенесли фокус сразу, то подписались бы на состояние сфокусированности редактора и при перемещении фокуса - сбрасывался бы и идентификатор текущей ячейки, что нам, разумеется, не надо.
Клавиатурная навигация
Постоянно тыкать мышью в ячейки для перехода между ними не очень-то удобно. Стрелочками на клавиатуре было бы быстрее. Традиционно в электронных таблицах есть два режима: режим навигации и режим редактирования. Постоянно переключаться между ними тоже напрягает. Поэтому мы сделаем ход конём и совместим редактирование и навигацию. Фокус будет постоянно оставаться на панели редактирования ячейки, но при зажатой клавише Alt, нажатие стрелочек, будет изменять редактируемую ячейку на одну из соседних. Для подобных выкрутасов есть специальный компонент $mol_nav, который является компонентом-плагином.
В $mol есть 3 вида компонент:
Добавляются плагины через свойство
plugins()
. Например, добавим клавиатурную навигацию нашему приложению:Тут мы указали, что навигироваться мы будем по горизонтали и по вертикали, по идентификаторам столбцов и колонок, соответственно. Текущие координаты мы будем синхронизировать со свойствами
current_col()
иcurrent_row()
, которые мы провяжем с собственноcurrent()
:Всё, теперь нажатие
Alt+Right
, например, будет делать редактируемой ячейку справа от текущей, и так пока не упрётся в самую правую ячейку.Копирование и вставка
Так как ячейки у нас являются ни чем иным, как нативными
td
dom-элементами, то браузер нам здорово помогает с копированием. Для этого достаточно зажатьctrl
, выделить ячейки и скопировать их в буфер обмена. Текстовое представление содержимого буфера будет ни чем иным, как Tab Separated Values, который легко распарсить при вставке. Так что мы смело добавляем обработчик соответствующего события:И реализуем тривиальную логику:
Славно, что всё это работает не только в рамках нашего приложения - вы так же можете копипастить данные и между разными табличными процессорами, такими как Microsoft Excel или LibreOffice Calc.
Выгрузка файла
Частая хотелка - экспорт данных в файл. Кнопку мы уже добавили ранее. Осталось лишь реализовать формирование ссылки на экспорт. Ссылка должна быть data-uri вида
data:text/csv;charset=utf-8,{'url-кодированный текст файла}
. Содержимое CSV для совместимости с Microsoft Excel должно удовлетворять следующим требованиям:После установки новой ссылки, мы форсируем запуск отложенных задач, чтобы произошёл рендеринг в dom-дерево до выхода из текущего обработчика событий. Нужно это для того, чтобы браузер подхватил свежесгенерированную ссылку, а не предлагал скачать предыдущую версию файла.
Формулы
Самое главное в электронных таблицах - не сами данные, а формулы, через которые можно связывать значения одних ячеек со значениями других. При этом за актуальностью вычисляемых значений электронная таблица следит сама, реактивно обновляя значения в ячейках зависимых от редактируемой в данный момент пользователем.
В нашем случае пользователь всегда редактирует именно формулу. Даже если просто вводит текст - это на самом деле формула, возвращающая этот текст. Но если он начнёт свой ввод с символа
=
, то сможет использовать внутри различные математические выражения и, в том числе, обращаться к значениям других ячеек.Реализовывать парсинг и анализ выражений - довольно сложная задача, а вечеринке уже мерещится ДедЛайн, так что мы не долго думая воспользуемся всей мощью JavaScript и позволим пользователю писать любые JS выражения. Но, чтобы он случайно не отстрелил ногу ни себе, ни кому-то ещё, будем исполнять его выражение в песочнице $mol_func_sandbox, которая ограничит мощь JavaScript до разрешённых нами возможностей:
Как видите, мы разрешили пользователю использовать математические функции и константы, а также предоставили пару функций: для получения формулы ячейки и вычисленного значения ячейки по её идентификатору.
Песочница позволяет нам преобразовывать исходный код выражения в безопасные функции, которые можно безбоязненно вызывать.
Заставлять пользователя писать вызов функции
result
вручную - слишком жестоко. Поэтому мы слегка изменяем введённую формулу, находя комбинации символов, похожие на кодовые имена ячеек видаAB34
, и заменяя их на вызовыresult
. Дополнительно, вместо значения, можно будет получить формулу из ячейки, приписав спереди собачку:@AB34
. Создание таких функций - не бесплатно, так что если в ячейке у нас просто текст, а не выражение, то мы так его и возвращаем безо всяких песочниц.Осталось дело за малым - реализовать свойство
result()
с дополнительной постобработкой для гибкости:Тут мы избавились от возможного значения
undefined
, а так же добавили преобразование строк похожих на числа в собственно числа.Финальный аккорд
На этом основная программа нашей вечеринки подходит к концу. Полный код приложения $mol_app_calc доступен на ГитХабе. Но прошу вас не спешить расходиться. Давайте каждый возьмёт по электронной таблице в свои руки и попробует сделать с ней что-нибудь эдакое. Вместе у нас может получиться интересная галерея примеров её использования. Итак...
The text was updated successfully, but these errors were encountered: