DotHTML 🔗 JavaScript
See original GitHub issueWe were discussing with @tomasherceg @martindybal how to connect JS code and dothtml markup.
We need/want several things:
- Invoke DotVVM
command
s andstaticCommand
s from Javascript code. - Invoke Javascript functions from event handlers.
- Work with the ViewModel from JS
- Have the functions scoped to one page. We would end up with crazy naming conventions and giant global declarations without the scoping.
- Register some control options for the page (for example a sorting strategy for
bp:AutoComplete
) - Register postback handers for one page
- Allow the same scoping for markup controls (and maybe also code controls)
We came with an idea, but there is still quite bit of issue to be solved (or ignored). You’d have a JavaScript object that would describe the client-side binding to the dothtml view. The describer object could be a default export of a module specified in a @js myPage
dothtml directive.
There may be more objects registered for one page, the page should inherit from its master page and controls should inherit from the view they are used in.
Options
The following properties should be supported:
init(HTMLElement)
- a function that is just invoked when the component or page is mounted into the DOM- this is not inherited into markup controls
- the functions are combined as described in https://blog.ploeh.dk/2017/11/06/function-monoids/ (well, just invoked after each other)
- basically equivalent to
dotvvm.events.init.subscribe(() => ...)
for pages
postbackHandlers: [PostbackHandler]
- a collection of postback handlers that are applied by default to all commands in this scope.- basically a scoped version of
dotvvm.globalPostbackHandlers
- the order is implicitly that master page is before page, page is before markup control. But it may be overridden by
before
andafter
fields of the postback handler.
- basically a scoped version of
jsCommands: { [name: string]: (element: HtmlElement, args: [KnockoutObservable<any>]) }
- a dictionary of client side command that may be invoked from the view (see JS Commands below)- the dictionaries are concatenated, in case of conflict page overrides master page, control overrides page.
requiredNamedCommands: [string]
names of commands that should be imported from the DotHTML into a context- this property is there to overcome the issue that command and staticCommands can be only created from DotHTML markup. Also from C# (if you know how to do that), but certainly not from JS.
- the command will be declared in the page/control in a special control
<dot:NamedCommand Name=nameOfTheCommand Command="{command: DoSomething()}"
- it will be later possible to invoke that command from JS.
- It will return a promise that will contain the result of the command (as compared to the well known hidden button hack)
- It will be possible to specify command arguments (as compared to the well known hidden button hack)
- This point is quite important, but it’s TODO how to represent that in the control/binding typesystem.
- This options is not inherited as described above, however the imported commands are inherited.
- TODO: extend this to support multiple command of the same name (for example with different data context). This can be done easily by distinguishing them by a set of optional ids.
Context
All function invoked from the options (init
and commands) will get additional argument with some context. The context will specifically contain:
importedCommands: { [name: string]: (args: [any]) => Promise<any>}
- Contains the commands imported from
<dot:NamedCommand ...
controls
- Contains the commands imported from
state: any
- in this property, you can store the state of this module. It will be isolated from other instanced of the same markup control.
JS Commands
We have to figure out how to ergonomically invoke the JavaScript commands from the dothtml markup.
One option is to have jsCommand
binding that will invoke exactly one command exported from the JS module. Syntax would be {jsCommand: nameOfTheCommand(Arg1, Arg2, ...)}
, where nameOfTheCommand
is any identifier and Arg1...
are the well-known value binding expressions. This way, you can invoke a JS function easily while the arguments are still written in ergonomic way (at least compared to manually unwrapping every knockout observable). The arguments are always passed as knockout observables that may be writable.
Note that all data must be passed arguments, the command does not implicitly get a reference to data context. This is to avoid having commands hidden dependencies that can’t be reused anywhere.
Another options is to have a function _js.Invoke<ResultValue>("commandName", Arg1, Arg2, ...)
that will be used from staticCommand
. The behavior will be very similar as for the jsCommand
.
There is always possibility to have both ways supported, as the first one is quite a bit more ergonomic and the second one it a more flexible…
Issues
- If we let the user work with the client-side viewmodel directly, it will get to an invalid state very easily and don’t do any validation here. For example, when you insert your own (naively created) model, client-side validation will stop working (as you’d not specify
$type
).- IMHO, we can’t leave so many traps in the API. We’ll have to give the user a proxy object to the view model that will handle the validation
- We’d like to keep the API strongly typed (in TypeScript), but that depends on the non-existent “TypeScript class generator”. This generator then depends on the dotvvm.compiler, which exists, but does not work very well (which is fixable) and needs to run after the build .NET (which is unfixable).
Edits: renamed commands
to jsCommands
, importedCommands
to requiredNamedCommands
, importedCommands
to namedCommands
, ExportCommand
to NamedCommand
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:16 (16 by maintainers)
Top GitHub Comments
About the exported commands - I meant something like this:
The command would not be exported globally, but in the
$context
variable. Then, we can introduce a JS API to get the command reference:The second argument would be a DOM element on which we’ll just
ko.contextFor
- I assume that in case of scoped commands the user will have some DOM element (button, link) anyway. And if the second argument is omitted, we can just look in the root scope.This was pretty much done in 3.0 and further extended in 4.0 with JsComponents and validation API. I don’t think there is much left to be done from this issue.