Хочу рассказать про dap — интересный и необычный язык реактивных правил для написания, в частности, веб-фронтендов.
Для затравки простая задачка: взять список неких пользователей (воспользуемся тестовыми данными, любезно предоставляемыми сервисом jsonplaceholder.typicode.com) и вывести их имена обычным html-списком; при нажатии на имя пользователя — показать алерт с его id.
Это, конечно, легко делается и на React, и на Angular, и на Vue. Вопрос: насколько легко? В dap это делается так:
'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users"
,'LI'.d("! .name").ui("? .id:alert")
)
*
для итерации по данным,:query
для асинхронной «конвертации» урла в полученные с него данные,!
для «печати» в генерируемый элемент,alert
, магически конверитрующий любые значения в алерт<UL class="users">
, но нам эти сложности с атрибутами и угловыми скобками ни к чему — dap использует лаконичную нотацию, очень похожую на CSS. Хотя и атрибуты при надобности в этой нотации возможны, например: 'FORM action=send.php method=post'
./posts?userId={id выбранного пользователя}
.'heroes'.d("$user=" // изначально $user не выбран
,'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users" // для каждого пользвателя из списка
,'LI' .d("! .name") // вывести содержимое поля .name
.ui("$user=$") // при нажатии выбрать этого пользователя
)
,'details'.d("*@ $user" // для выбранного $user выполнить следующее:
,'H2'.d("! .id `: .name") // вывести его .id и .name,
,'A'.d("!! .email@ (`mailto: .email)concat@href") // дать активную ссылку на .email,
,'posts'.d("* (`https://jsonplaceholder.typicode.com/posts? .id@userId )uri:query" // и показать его посты
,'H3'.d("! .title")
,'p'.d("! .body")
)
)
)
'details'
(что в переводе н HTML означает <DIV class="details">
) от того, какой пользователь выбран. Выбранный пользователь, таким образом, оказывается переменной состояния. Такие переменные в dap-правилах обозначаются префиксом $
(как s в «state»). При нажатии на любой из элементов LI изменяется содержимое переменной $user
, на что элемент 'details'
автоматически реагирует и обновляется.'details'
узнает, что надо бы обновиться? Очень просто: в его правиле генерации присутствует обращение к переменной $user
, а в правиле реакции элемента LI эта переменная как раз и изменяется..d
— исполняются на фазе построения элемента. Правила реакции задаются методом .ui
и исполняются при взаимодействии пользователя с элементом. Это два самых часто употребимых типа правил; кроме них есть еще несколько типов, но о них как-нибудь потом..,$@:`(){}
и пробел, все остальное может использоваться свободно. В частности, идентификаторы могут содержать, или состоять целиком, например, из символов !?*
и т.п. Кому-то это покажется дикостью, но на деле это очень, очень удобно. Например, самыми часто используемыми в dap являются операторы (кстати, имена операторов — тоже идентификаторы):!
— вывод значения в элемент, что-то вроде print!!
— установка свойства (атрибута) элемента, что-то вроде setAttribute?
— условный оператор, что-то вроде if*
— мультиплексор, или итератор, что-то вроде for$?
(где $
— это префикс переменной состояния, а собственно имя состоит просто из знака вопроса). Мне просто лень придумывать им имена, а потом еще и печатать их в нескольких местах. При этом никогда не возникает никаких сложностей с пониманием, что эта переменная означает в каждом конкретном месте: благодаря компактности и обозримости dap-кода, вся область действия такой переменной обычно умещается в несколько строк, и всегда понятно что к чему.'toggle'.d("$?=" // определяем $? и инициируем ее 'ничем' (удобная разновидность false)
,'BUTTON'.d("! `Toggle").ui("$?=$?:!") // кнопкой инвертируем $?, как в x=!x
,'on'.d("? $?; ! `?") // показать галочку если $? не пустой
)
`
(backtick, на клавиатурах обычно под клавишей Esc). Например, элемент BUTTON в примере выше подписан литералом `Toggle
. В отличие от других, «нормальных» языков, где, скажем, строковые литералы заключаются в кавычки и могут содержать приличные объемы текста, в dap-правилах литералы обрамляются только с одной стороны (тем самым префиксом `
), и не могут содержать пробелов, т.к. пробел служит разделителем токенов («аргументов») в правиле. Как же так, спросите вы? А вот так. Литералы в dap предназначены не для эпистолярных фрагментов, а для различных коротких кодов: номеров, меток, каких-то отладочных заглушек и.п. Текстовые данные (как, впрочем, и любые другие) dap настойчиво требует хранить в виде констант в специальном словаре, в секции .DICT
(от «dictionary», понятное дело):'whoami'.d("$fruit="
,'H3'.d("! ($fruit msg.fruit-selected msg.please-select)?! $fruit")
,'UL'.d("* fruit"
,'LI'.d("! .fruit").ui("$fruit=.")
)
)
.DICT({
msg :{
"please-select": "Кто я? Зачем я в этом мире?",
"fruit-selected": "Я — "
},
fruit :["Апельсинчик, сочный витаминчик", "Яблочко зеленое, солнцем напоённое", "Cлива лиловая, спелая, садовая", "Абрикос, на юге рос"]
})
'multiselect'.d("$color= $size="
,'H3'.d("! (($color $size)? (selected $color $size)spaced not-selected)?!")
,'size'.d("$!=select(sizes@options)").u("$size=$!.value") // использовать шаблон select с данными из sizes
,'color'.d("$!=select(colors@options)").u("$color=$!.value") // использовать шаблон select с данными из colors
)
.DICT({
select: 'SELECT'.d("* .options@value" // этот шаблон используется выше
,'OPTION'.d("! .value")
).ui(".value=#:value"),
sizes: ["XS","S","M","L","XL"],
colors: "white black brown yellow pink".split(" "), // когда лень писать массив
"not-selected": "Select size and color please",
selected: "Selected specs:"
})
'main'.d("! header content footer")
.DICT({
header : 'HEADER'.d(...),
content : 'UL.menu'.d(...),
footer : 'FOOTER'.d(...)
})
colors
генерируется из строки с помощью метода split. Можно вообще весь объект для словаря импортировать из внешнего скрипта-библиотеки любым доступным способом — хоть по старинке через <script src="...">
или XHR->eval(), хоть через import
(но убедитесь, что ваши клиенты этот новомодный способ поддерживают). Секций .DICT
может быть несколько, все они объединяются в один общий словарь.const lib1={
message: "Hello from lib1"
},
lib2={
message: "Hi from lib2"
};
'my-app'.d("! message wrapped.message imported.message")
.DICT(lib1)
.DICT({
wrapped: lib2,
imported: import "extern.js";
})
//файл extern.js
({
message: "Bonjour de lib importe"
})
.USES
'main'.d("$?=" // unset $?
,'BUTTON.toggle'.ui("$?=$?:!") // toggle $?
,'activated'.d("? $?" // only show when $? is set
,'imported'.d("! delayed.message") // now print the message from the lib
)
)
.USES({
delayed: "extern.js"
})
:query
асинхронны и не задерживают отображение и работу остальных элементов.$a:x,y,z
соответсвует z(y(x($a)))
в си-подобной записи. То, что вход у конвертора всего один, казалось бы, ограничивает его возможности по сравнению с «обычной» функцией. Но это не так. Конвертор может принимать и отдавать как элементарные значения, так и объекты/массивы (в javascript разница между этими понятиями размыта), содержащие любое количество данных. Таким образом, конверторы в dap полностью заменяют «традиционные» функции, при этом могут собираться в цепочки и могут быть асинхронными, не требуя при этом никакого дополнительного синтаксического оформления.()?
возвращает первый непустой аргумент или самый последний (аналог ||-цепочки в javascript), а агрегатор ()!
— наоборот, первый пустой аргумент, или самый последний (аналог &&-цепочки в javascript). Или, например, агрегатор ()uri
— строит из аргументов параметризованный URI.!
добавляет значение аргумента к содержимому элемента, а оператор !!
устанавливает атрибуты элемента) или управлять ходом исполнения правила (как, например, условный оператор ?
).FUNC
(от «functionality»):'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users"
,'LI'.d("! (.name .username:allcaps .address.city):aka,allcaps")
.ui("(.address.street .address.suite .address.city .address.zipcode)lines:alert")
)
.DICT({
"no-contact": "No contacts available for this person"
})
.FUNC({
convert: {
allcaps: o=> o.toUpperCase(),
aka : o => o.name + " also known as "+o.username+" from the city of "+o.city
},
flatten:{
lines : values=>"Address:\n" + values.reverse().join("\n")
}
})
.FUNC
прописывать только протокол взаимодействия между этим модулем и dap. Скажем, в примере с игрой в крестики-нолики dap.js.org/samples/tictactoe.html (по мотивам React-туториала), вся логика игры описана в отдельном замыкании, а dap только связывает эту логику с картинкой на экране.<script src="https://cdn.jsdelivr.net/gh/jooher/dap/0.4.min.js">
К сожалению, не доступен сервер mySQL