A simple task for the seed: take a list of certain users (we will use the test data kindly provided by jsonplaceholder.typicode.com ) and display their names in a regular html-list; when you click on the username - show an alert with its id.
This, of course, is easily done on React, Angular, and Vue. Question: How easy is it? In dap, this is done like this:
'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users" ,'LI'.d("! .name").ui("? .id:alert") )
(* This and subsequent dap examples can be interactively tested in the dap.js.org/test.html sandbox)
This is the first trivial task that came to mind and the trivial way to solve it. It’s convenient to write on the dap “forehead”, without making a fuss of classes, components and other ritual props. What I see, I sing. But I ’m “singing” not in javascript or its derivatives, but in the language of dap rules , specially tailored to a simple and concise description of reactive dependencies between elements.
In the example above, however, there are no dependencies yet. But there is:
-
*
operator for iterating over data, - converter
:query
for asynchronous "conversion" of url into data received from it, - operator
!
to "print" into the generated element, -
alert
converter that magically converts any value to alert
If you are familiar with HTML, then the words UL and LI are probably known to you. Yes, these are tags for an unordered list and for a list item, respectively. And if CSS is no stranger to you, then you should understand the meaning of the UL.users notation: this is an UL element with the "users" class. This would be written in HTML
<UL class="users">
, but we don’t need these difficulties with attributes and angle brackets - dap uses concise notation, very similar to CSS. Although attributes, if necessary, are possible in this notation, for example:
'FORM action=send.php method=post'
.
By the way: did you notice that this example is written in plain javascript, and the dap rules themselves are just strings? And element signatures ("tags") are also strings, just in single quotes. Yes, the code of the dap application looks unusual, but it is pure javascript ready for use: it does not need to be transformed from source codes in newfangled languages ​​(which, of course, will not prevent you from using these fashionable languages ​​if you wish).
Let's complicate the task a bit by adding dependencies: suppose that when clicking on the user’s name, his id and name appear a little in the H2 element, below them there will be an email, as well as all his “posts”, which are taken from
/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") ) ) )
Here we already have a dependency on the contents of the
'details'
element (which in HTML translation means
<DIV class="details">
) on which user is selected. The selected user is thus a state variable. Such variables in dap rules are indicated by the
$
prefix (like s in "state"). Clicking on any of the LI elements changes the contents of the
$user
variable, to which the
'details'
element automatically responds and updates.
In dap, the program structure determines immediately both the state model and the model of user interaction (display and reactions). On the one hand, this allows you to write beautiful, concise and understandable code, but on the other hand, it requires a clear hierarchical structure of the application. Personally, I consider this requirement a useful limitation: it makes (and helps) more carefully study the application logic and get rid of logical inconsistencies.
How does
'details'
know what needs to be updated? It is very simple: in its generation rule there is an appeal to the
$user
variable, and in the reaction rule of the LI element this variable just changes.
Generation rules - those that are specified using the
.d
method - are executed at the element construction phase. Reaction rules are set using the
.ui
method and are executed when the user interacts with the element. These are the two most commonly used types of rules; besides them there are several more types, but about them somehow later.
The syntax for dap rules is quite specific. It has nothing to do with all the familiar C-like syntax, so at first it may seem strange and incomprehensible. But in fact, it is extremely simple and, I am not afraid of this word, beautiful.
There are no keywords in the language. The characters
.,$@:`(){}
And a space are reserved, everything else can be used freely. In particular, identifiers can contain, or consist entirely of, for example, characters
!?*
, Etc. This will seem wild to someone, but in fact it is very, very convenient. For example, the most commonly used dap are operators (by the way, the names of the operators are also identifiers):
-
!
- outputting a value to an element, something like print -
!!
- setting the property (attribute) of an element, something like setAttribute -
?
Is a conditional statement, something like if -
*
- multiplexer, or iterator, something like for
The names of regular operators are deliberately chosen as nonverbal. Firstly, they are just shorter - just one character. Secondly, they are well distinguishable from, say, the names of constants and variables. And finally, they are neutral to the locale and look equally appropriate in programs in any national language, at least English, at least Russian, at least Chinese (good, javascript and unicode allow this).
And who did not encounter a situation when you sit and stupid, inventing a name for a variable? Believe it or not, in my dap-code almost all "boolean" (yes / no) variables are called
$?
(where
$
is the prefix of the state variable, and the name itself consists simply of a question mark). I’m just too lazy to come up with their names, and then also print them in several places. In this case, there is never any difficulty in understanding what this variable means in each particular place: due to the compactness and visibility of the dap code, the entire scope of such a variable usually fits in several lines, and it is always clear what’s what.
'toggle'.d("$?=" // $? '' ( false) ,'BUTTON'.d("! `Toggle").ui("$?=$?:!") // $?, x=!x ,'on'.d("? $?; ! `âś“") // $? )
Of course, this is just my personal style. If you come from the world of Enterprise Java, do not worry: no one will forbid you to use arbitrarily long identifiers anywhere. What can not be said about literals.
Literals in dap rules are indicated by the prefix
`
(backtick, on keyboards usually under the Esc key). For example, the BUTTON element in the example above is signed with the literal
`Toggle
. Unlike other “normal” languages, where, say, string literals are enclosed in quotation marks and can contain decent amounts of text, in dap rules, literals are framed only on one side (thereby the
`
prefix), and cannot contain spaces, t .to. the space serves as a separator of tokens (“arguments”) in the rule. How so, you ask? And like this. Literals in dap are not intended for epistolary fragments, but for various short codes: numbers, labels, some kind of debugging stubs, etc. Text data (as well as any other data) dap insistently requires to be stored as constants in a special dictionary, in the
.DICT
section (from “dictionary”, of course):
'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 , , ", ", "] })
The obvious advantage of storing all texts in a dictionary is, for example, the ease of subsequent localization in other languages.
But the dictionary stores not only text data, but generally any reused entities. Including parameterizable templates, or “components”, as they are called in other frameworks:
'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:" })
In principle, nothing prevents the whole application from being broken down into “components” and stuffed into a dictionary if you like this style:
'main'.d("! header content footer") .DICT({ header : 'HEADER'.d(...), content : 'UL.menu'.d(...), footer : 'FOOTER'.d(...) })
Personally, I don’t see much sense in this, and usually I put out only fragments that are actually used repeatedly.
By the way, since this is still ordinary javascript, and the dictionary itself is, as you can see, just an object (or, as they are also called, an associative array), you can also create it in any legal way for javascript. For example, in the example just above, an array of values ​​for the
colors
constant is generated from the string using the split method. You can generally import the entire object for the dictionary from an external script library in any way possible - at least in the old fashion through
<script src="...">
or XHR-> eval (), at least through
import
(but make sure your clients are newfangled support method).
.DICT
can be several
.DICT
; all of them are combined into one common dictionary.
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 importé" })
In addition to the standard javascript tools, dap has its own mechanism for connecting external libraries. Its difference is that libraries are loaded lazily - only when you really need to take something from the library. Such libraries are indicated in the
.USES
section
'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" })
Here, the extern.js library will be loaded only after clicking the button, when it is necessary to display the 'imported' element - and for this purpose, parse and compile its dap rule, which refers to an external library.
Yes, dap rules are “compiled” before execution. And lazy, only at the first actual call to the element template. This laziness allows you to “smear” the compilation of a large application into many small stages, and not to occupy browser resources with unused sections of code. Of course, such a concern for resources is relevant only for some very large applications, or weak devices. In general, I can say that dap is lightning fast (on a UI scale, of course) - both in compilation and in execution. Performance can be controlled in the browser console: the time of each reaction is logged there.
The only noticeable delays that can actually occur are network delays. But these delays do not block the dap application. Converter web requests
:query
asynchronous and do not delay the display and operation of other elements.
By the way, what is a converter ? The converter in dap is just a function of the form value → value , that is, with one input and one output. Such a restriction allows constructing chains from converters, for example, the expression
$a:x,y,z
corresponds to
z(y(x($a)))
in a s-like record. The fact that the converter has only one input, it would seem, limits its capabilities in comparison with the "normal" function. But this is not so. The converter can accept and give both elementary values ​​and objects / arrays (in javascript, the difference between these concepts is blurred) containing any amount of data. Thus, converters in dap completely replace the "traditional" functions, while they can be assembled in chains and can be asynchronous, without requiring any additional syntactic design.
There are no traditional "functions" with a fixed list of parameters in dap, respectively - as unnecessary.
But there are still aggregators - functions of an arbitrary number of arguments (usually peer, but not always). For example, aggregator
()?
returns the first non-empty argument or the very last (analogue of the || -chain in javascript), and the aggregator
()!
- on the contrary, the first empty argument, or the most recent (analog of && - chains in javascript). Or, for example, the aggregator
()uri
- builds a parameterized URI from arguments.
And the last type of function in dap is operators . If converters and aggregators are ideologically closer to “pure” functions that simply calculate values ​​(although this is not always the case), then the task of operators is to apply these values ​​to the generated element (for example, the operator
!
Adds the value of the argument to the contents of the element, and the operator
!!
sets the attributes of the element) or control the progress of the rule (like, for example, a conditional statement
?
)
The arsenal of full-time converters, aggregators and operators in dap is minimal, but it doesn’t matter. The main thing is that you can create your own! This is an important point. You need to clearly understand that the language of dap-rules does not at all pretend to be a universal programming language, it only describes the dependencies between the elements. It is understood that hardcore and computations are implemented by native means of the environment (for the browser, this is, of course, javascript) and dap plays a purely "logistic" role - indicating what, when and from what to do and where to give.
Native functionality is described in the
.FUNC
section (from "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") } })
Here, its own functionality is trivial, therefore it is written directly "in place". Of course, it makes sense to write something more complex as an independent module, and to register only the protocol of interaction between this module and dap in .FUNC. For example, in the tic-tac-toe game dap.js.org/samples/tictactoe.html (based on the React tutorial), the entire logic of the game is described in a separate closure, and dap only connects this logic with the picture on the screen.
You can learn more about dap at dap.js.org
Yes, one more thing. The entire dap engine is compressed (
<script src="https://cdn.jsdelivr.net/gh/jooher/dap/0.4.min.js">
) weighs no less than 9 kB. Nine kilobytes.