Capturing keystrokes

64 views
Skip to first unread message

Stephen Schaefer

unread,
Aug 22, 2015, 7:06:04 PM8/22/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
I wanted to capture, for example, Ctrl-i while in a text box, and even with the advice from this group found here, I struggled a bit.  In case it helps anyone else, this is what I came up with:

%% -*- mode: nitrogen -*-
%% vim: ts=4 sw=4 et
-module (example).
-compile(export_all).
-include_lib("nitrogen_core/include/wf.hrl").

main
() -> #template { file="./site/templates/bare.html" }.

title
() -> "Example".

body
() ->
   
TextID = wf:temp_id(),
    wf
:wire(#api{ name=control_key, tag="foo" }),
    wf
:wire(TextID, #event {
    type
= keypress,
   
% key "i" == 105
    actions
= #script { script="if (event.ctrlKey && ([105].indexOf(event.which) >= 0)) {" ++
       
"event.preventDefault();" ++
       
"event.stopPropagation();" ++
       
"page.control_key('" ++ TextID ++ "', event.which);}" }
   
}),
   
[
   
#flash{},
   
#textbox{ id = TextID, text = "initial text" }
   
].

api_event
(control_key, _Tag, [TextID, KeyCode]) ->
   
Msg = wf:f("Control key ~p pressed in ~p", [KeyCode, TextID]),
    wf
:flash(Msg).


I'd be grateful for any suggestions for improvement.

Stephen Schaefer

unread,
Aug 22, 2015, 10:16:16 PM8/22/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
My next trick was going to be to move the keypress capture to a custom element, but I'm not having any luck.  Here's the invocation of the element:

%% -*- mode: nitrogen -*-
%% vim: ts=4 sw=4 et
-module (example).
-compile(export_all).
-include_lib("nitrogen_core/include/wf.hrl").
-include_lib("records.hrl").


main
() -> #template { file="./site/templates/bare.html" }.

title
() -> "Example".

body
() ->

   
[
   
#flash{},
   
#example{}
   
].


records.hrl:

%% Include the automatically generated plugins directory
-include("plugins.hrl").

%% Include any application-specific custom elements, actions, or validators below

-record(example, {?ELEMENT_BASE(element_example),
        attr1
:: any(),
        attr2
:: any()
   
}).

and element_example.erl:

%% -*- mode: nitrogen -*-
%% vim: ts=4 sw=4 et
-module (element_example).
-include_lib("nitrogen_core/include/wf.hrl").
-include("records.hrl").
-export([
    reflect
/0,
    render_element
/1,
    api_event
/3
]).

%% Move the following record definition to records.hrl:
-spec reflect() -> [atom()].
reflect
() -> record_info(fields, example).

-spec render_element(#example{}) -> body().
render_element
(_Record = #example{}) ->
    wf
:wire(#api{ target=page, name=control_key, tag="foo", delegate=?MODULE }),
   
TextID = wf:temp_id(),

    wf
:wire(TextID, #event {
    type
= keypress,
   
% key "i" == 105
    actions
= #script { script="if (event.ctrlKey && ([105].indexOf(event.which) >= 0)) {" ++
       
"event.preventDefault();" ++
       
"event.stopPropagation();" ++
       
"page.control_key('" ++ TextID ++ "', event.which);}" }
   
}),

   
#textbox{ id = TextID, text = "initial text" }.

api_event
(control_key, _Tag, [TextID, KeyCode]) ->
   
Msg = wf:f("Control key ~p pressed in ~p", [KeyCode, TextID]),
    wf
:flash(Msg).

Unfortunately, loading the page results in

TypeError: obj(...) is undefined
http
://localhost:8000/example
Line 21

...where line 21 consists of

Nitrogen.$anchor('.wfid_temp626544', 'page');obj('.wfid_temp626544').control_key = function() {var s = Nitrogen.$encode_arguments_object(arguments);

...which baffles me, since by my reading, nitrogen defines obj() in global - or at least document - scope.  I tried replacing the wf:wire() of #api{} with wf:defer(), but the behavior didn't change.

Stephen Schaefer

unread,
Aug 23, 2015, 12:14:10 AM8/23/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
OK; it's not the function obj() that is undefined; the function obj() is *returning* undefined.  And, indeed, the "wfid_temp999999" class it's searching for is not attached to anything I can discover.  The code fetches that class from action_api.erl's

render_action(Record) ->
     Anchor = Record#api.anchor,

....so now I need to figure out how whether Record#api.anchor was incorrectly set; if so, whether it should be set to the containing element or to 'page' or to something else; if it was correctly set, then why was no HTML element was emitted with that class, and how to discover that element so that instead of "page.control_key" I can use "discover_element().control_key".

Jesse Gumm

unread,
Aug 23, 2015, 12:56:28 PM8/23/15
to nitrogenweb
I believe the problem is here:

wf:wire(#api{ target=page, name=control_key, tag="foo", delegate=?MODULE }),

target has no meaning with respect to #api. The short recomendation is to do:

wf:wire(page, #api{ name=control_key, tag="foo", delegate=?MODULE }),

This will both anchor the api to the page *AND* set the target to the page.

Anchor and target can be a bit confusing. Probably the easiest way to
illustrate the target/anchor difference is a validator.

Doing wf:wire(#validate{anchor=my_button, target=my_field})

Will attach the validator to 'my_button', but perform the validation
on the 'my_field' element.

In the case of #api, you can safely leave target alone, and set the
anchor, and it should work.

-Jesse
> --
> You received this message because you are subscribed to the Google Groups
> "Nitrogen Project / The Nitrogen Web Framework for Erlang" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to nitrogenweb...@googlegroups.com.
> To post to this group, send email to nitro...@googlegroups.com.
> Visit this group at http://groups.google.com/group/nitrogenweb.
> For more options, visit https://groups.google.com/d/optout.



--
Jesse Gumm
Owner, Sigma Star Systems
414.940.4866 || sigma-star.com || @jessegumm

Stephen Schaefer

unread,
Aug 23, 2015, 7:45:16 PM8/23/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
Thanks for looking at this.  I tried changing the suspect line to be as you suggested (no other lines changed), and the emitted javascript is still calling obj() with an unassigned class.  The generated HTML is:

<!DOCTYPE html>
<html lang="en" style="height:100%">
<head>
 
<meta charset="utf8">
 
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 
<title>Example</title>
 
<script src='/nitrogen/jquery.js' type='text/javascript' charset='utf-8'></script>
 
<script src='/nitrogen/jquery-ui.min.js' type='text/javascript' charset='utf-8'></script>
 
<script src='/nitrogen/livevalidation.min.js' type='text/javascript' charset='utf-8'></script>
 
<script src='/nitrogen/nitrogen.min.js' type='text/javascript' charset='utf-8'></script>
 
<script src='/nitrogen/bert.min.js' type='text/javascript' charset='utf-8'></script>
 
<link rel="stylesheet" href="/nitrogen/jquery-ui/jquery.ui.all.css" type="text/css" media="screen" charset="utf-8" />
 
<link rel="stylesheet" href="/nitrogen/nitrogen.css" />
 
<link rel="stylesheet" href="/css/style.css" />
</head>
<body style="height:98%">
<div class="flash_container wfid_temp940150 wfid_flash" ></div><input type="text" class="wfid_temp940183 wfid_temp940174 textbox" value="initial text" />
<script>
Nitrogen.$set_param('pageContext', 'N9OnBGyMtod_jyIdEDtm9FTDXk0ZNmZg5qO03hoR9aD1m_B8VOcHtlOrZwGx_ZDaogeSFqKrOU7Hi4i4WKRDIWIHjwoGmKSY-TWvFUkxTJ9HbGEktJDRk7AZFV4ZYQ_9JbONFsQ6QLesk9JwvA1Kzb1u8_HHbvNYe0KaYl4xvDKG2PQiqS5wtzE5l5qjrNPkj7OYUqVdkSM3XHR9ISt_IUDj-MzS3J7wih0HWIVs5gtSkcyRtJEQdny7riUU2AhafckM8HduzmCY0ETS0_Pf73r4hIhJv-FWFnWPI7oHEmtbQPmS');

Nitrogen.$anchor('.wfid_temp940170', 'page');obj('.wfid_temp940170').control_key = function() {var s = Nitrogen.$encode_arguments_object(arguments);
Nitrogen.$anchor('.wfid_temp940170', 'page');Nitrogen.$queue_event('page', null, 'gZuitgwcYOU4BmWxqacINjsuOS5nHuj4maDJVHKZOsx23vQIPMedssO-uBTuOMLQlW4pnNjCREMw3wMspGMuZsnMiWCB-KENpCtSlyLzKq2FncK0AIcEKuZI68WohFZC7IbEoyLXi4cQ_t5LCm6fP60n2RIBhAxM1yIKHu_X15RENS8XodJJpWwrUszcTbVuuDCjHIJ7MWhxWdAbTZtBhnmZAb5Zp71tVtiwmZ-59Uu3Po2VX6B6iCKIY0xhwMDJpu_EN4YEiqpbfgxm-f3lnXl8IKCJ3qIzNO-IsqTyheDYYJAcWoPSLM8e8JSD7nLSqKyGrr-kiWP4pQkr3LjcPFjKAOL4GVf7gT4WtC4cn-QgNmnGJjf_haXNkAIvWuBE_Gbc2Q', s);};
Nitrogen.$anchor('.wfid_temp940170', '.wfid_temp940174');Nitrogen.$observe_event('.wfid_temp940170', '.wfid_temp940174', 'keypress', function anonymous(event) {
Nitrogen.$anchor('.wfid_temp940170', '.wfid_temp940174');
Nitrogen.$anchor('.wfid_temp940170', '.wfid_temp940174');if (event.ctrlKey && ([105].indexOf(event.which) >= 0)) {event.preventDefault();event.stopPropagation();page.control_key('temp940174', event.which);}});
</script>
</body>
</html>

Note that wfid_temp940170 appears nowhere as a class - only wfid_temp940150 and wfid_temp940147.  The keyword 'page' gets treated specially by obj():
    if (k == 'page' || k == '.page') {
       
return jQuery(document)
   
}

I also tried "wf:wire(page, page, #api{name=control_key, tag="foo"}," within the custom element, but the result was the same.

Stepping back from the problem: what I want is a #event{} to fire on keypress (or perhaps keydown or keyup), and then conditionally do a postback that encapsulates the state of event.which and event.ctrlKey (and potentially event.metaKey or event.shiftkey).  #api{} provides a way to generate a postback from within #script{}, which is why I explored that path.  Is there a different way to generate such a postback?  I can imagine circumstances where one would want to distringuish ctrl-click, shift-click, meta-click and ctrl-shift-click (why, yes, I do speak emacs ;-).

In an alternate universe, the "type" argument to #event{} would take an arbitrary script that would return {boolean(), term()} - the boolean specifying whether this was the event of interest, and the term being data available to send in the postback.  I'll let you know when I've implemented that.  Don't hold your breath :-).

Thanks again for your attention.

Jesse Gumm

unread,
Aug 24, 2015, 9:46:32 PM8/24/15
to nitrogenweb
Hi Stephen,

I did some experimenting with creating an #api from within a custom
element, and it turns out to point out a bit of a bug. I'll have to
work on the fix.

But for you, doing your #api as the following will fix it:

wf:wire(#api{anchor=page, ... }).

Just make sure you set anchor=page in the #api element itself.

Thanks for pointing this out!

-Jesse

Jesse Gumm

unread,
Aug 24, 2015, 9:53:37 PM8/24/15
to nitrogenweb
Oops, looks like my explanation of anchor and target were wrong. Shows
how I shouldn't answer questions when I'm distracted (was running a
tournament when I sent the first response). The arguments for
wf:wire() are wf:wire(Trigger, Target). Anchor must be specified in
the action record itself, thus why #api{anchor=page} is the proper
solution.

My bad for the misinformation.

-Jesse

Jesse Gumm

unread,
Aug 24, 2015, 9:57:06 PM8/24/15
to nitrogenweb
Also relevant:

I've added a test verifying that #api works from within an element
this way: https://github.com/nitrogen/NitrogenProject.com/commit/cf707c77884103383a660af9c81e2512e21fc84e

Stephen Schaefer

unread,
Aug 25, 2015, 12:22:21 AM8/25/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
Thanks so much for looking at this - #api{ anchor=page ...} within a custom element works!

The element I'm writing (not the example earlier - that was strictly to focus for clarity) is by its nature iterative/recursive: an editable outliner.  I've noticed no trouble in my current light, manual testing, but do you foresee trouble if the wf:wire(#api{..}) gets called hundreds of times?  Do later calls replace earlier ones, or mask them?  If the latter, should I worry that the cumulation comprises a memory leak?  They do not, in my observation, fire multiple times - a good thing!

Also, thanks for highlighting your testing code - I'm going to study that for application to my own project (definitely not yet ready for use).



Jesse Gumm

unread,
Aug 25, 2015, 12:31:28 AM8/25/15
to nitrogenweb
Is wf:wire(#api{..}) going to be called hundreds of times in the same
page by the same client, replacing the definition of the api calls
live?

That's not a use-case I've explored before, and I'm not entirely sure
of the ramifications of it. My gut is telling me the client might get
bogged down (kinda like how I find gmail ends up eating up a lot of
RAM here when left open for days at a time).

What would probably be better would be to create a single #api call on
the page, and then use jquery to bind events to the certain elements
you want to be able to trigger the api call.

Like if you did something like:

$.on("keypress", function() { wrap_up_event_and_call_api()}, ".triggerclass"),

Then when you add or remove elements like
#textbox{class=triggerclass}, it will effectively call
wrap_up_event_and_call_api() for each keypress, (obviously modifying
this as necessary to capture the info you want from the event itself).

That way you're not replacing the api definition over and over, but
still allowing yourself to dynamically add elements that will trigger
the api.

Whaddya think?

ll...@writersglen.com

unread,
Aug 25, 2015, 11:37:34 AM8/25/15
to nitro...@googlegroups.com, Nitrogen Project / The Nitrogen Web Framework for Erlang
Hi Stephen,

Do you plan to publish code for your ediytle outliner. This might fit well in a project I'm currently working on.

All the best,

Lloyd R. Prentice

--

Stephen Schaefer

unread,
Aug 26, 2015, 1:01:03 AM8/26/15
to Nitrogen Project / The Nitrogen Web Framework for Erlang
Yes, I will be publishing.  It is *not* currently in a usable state.

Currently, I can
   * display the roots of the outline (graph)
   * expand a node with children
   * contract a node with children
   * change the text of a node headline
   * change the text of a node body
   * insert a new node (headline, body, no children) (^I)

If you've used the desktop version of Adobe Reader, you'll be familiar with the outline in a pane on the left and the body text in a pane on the right.  Imagine being able to edit that.  Now imagine the the outline is actually the projection of a graph.

What I intend to accomplish:
   * undo the last node change (^Z)
   * delete a (pointer to a) node
   * insert a (pointer to a ) node
   * demote a headline (remove its point from the parent list of children and add its pointer to the siblings list of children)
   * promote a headline (remove the pointer to the node from its parent and insert a poiner to the node as the next sibling of the parent)
   * deep copy a node - new copies of the node and of every unique child, with pointers mapped to the new children
   * shallow copy a node - new copy of the headline and its text, but child pointers are to the original children
   * clone a node - parent pointer to this node is duplicated
   * run Leo-style "tangle" operations on the graph - see http://leoeditor.com/

In a distant future, I'll have parsers to convert source code into appropriate graphs, and the node "text" will optionally be HTML elements or images.

My project differs in two fundamental respects from the Leo editor:
   * it models a generalized directed ordered graph instead of a directed acyclic ordered graph (ordering is on the edges of the node, e.g., {(a,b), (a,c), (a,b)} differs from {(a,b), (a,b), (a,c)}
   * its storage format is intended to be version control friendly (clearly demarcated text files per node) as opposed to version control hostile (massive machine oriented XML - a merge conflict will ruin your day).

Less fundamentally, it's written in Erlang instead of Python.

This is not my day job.  If I'm lucky I'll have something worth while within the decade.

If the above remains of interest to you, I'll get my current stuff into github.

    - Stephen

T0ha

unread,
Sep 6, 2017, 4:26:47 AM9/6/17
to nitro...@googlegroups.com
Hi all,
I’m trying to integrate PayPal Express button to my Nitrogen app. So I add  #api{} to the page, then call page.setup_transaction(smth) from ‘payment’ function of button object and want to get transaction data back. 
But seems there are no way to return anything from call to api. 

Can you suggest me smith? Thx

Best wishes, Anton Shvein&
 
26 авг. 2015 г., в 10:01, Stephen Schaefer <s.schaef...@gmail.com> написал(а):

Jesse Gumm

unread,
Sep 6, 2017, 9:39:23 PM9/6/17
to nitrogenweb
Hi Anton,

The #api{} element and the relevant page.some_function() is just a wrapper for a custom postback, so to actually return a value from an #api{} call isn't really possible.

You could look at wrapping it in a promise (something I'll admit I haven't done).

The way I more commonly deal with that is to just know that a call to an #api{} created function is just a postback, so that in the api_event/3 function, you can wire calls to continue to the next step.  Maybe it's a matter of wrapping up some kind of data either with JSON or Bert, but it shouldn't be too much trouble.

Let's say you've got two steps here that look like this:

Consider the #api{}:

wf:wire(#api{name=process_things}).

Then in your JS:

function do_something_step1() {
   var someval=get_some_value();
   var some_otherval = get_some_other_value();
   page.process_things(someval, some_otherval);
}

function do_something_step2(some_val_from_server, someval, some_otherval) {
  // do whatever
}


And in your Module the following api_event function:

api_event(process_things, _, [SomeVal, SomeOtherVal]) ->
  SomeServerVal = get_some_server_val(),
  wf:wire(#js_fun{function=do_something_step2, args=[SomeServerVal, SomeVal, SomeOtherVal]}).


That, largely, would be what you would need.

-Jesse


To unsubscribe from this group and stop receiving emails from it, send an email to nitrogenweb+unsubscribe@googlegroups.com.

To post to this group, send email to nitro...@googlegroups.com.
Visit this group at http://groups.google.com/group/nitrogenweb.
For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "Nitrogen Project / The Nitrogen Web Framework for Erlang" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nitrogenweb+unsubscribe@googlegroups.com.

To post to this group, send email to nitro...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages