ng2-pdf-viewer / pdfjs annotation change listener - pdf.js

ng2-pdf-viewer provides a pdfjs PDFDocumentProxy on load with some useful properties such as getFieldObjects(). These return the annotation layer fields with their initial values.
I however can't find an observable to subscribe onto changes. There are several eventBus._listeners objects when I console.log the PDFDocumentProxy during the onLoad function, but I don't think any of these trigger on input change.
Is there a better way to analyze pdf.js annotation fields than to iterate over all field objects and read the html inputs like:
public async onLoad(pdf: PDFDocumentProxy): void {
const obj = await pdf.getFieldObjects();
const names: string[] = [];
Object.keys(obj).forEach((o: string) => names.push(
...obj[o].filter(element => element['name'])
.map(x => x['name'])));
names.forEach((name: string) => (document.getElementsByName(name)[0] as HTMLInputElement).value;
}

Related

How to best access data from QueryRenderer in a parent component in Relay Modern?

As you can see from the picture below, I'm rendering the popover using a react-relay QueryRenderer since the request is slow, and I do not want the rest of the page to wait for events to be fetched.
My problem is that in the navigation I have a button to show/hide the popover. That button should only be rendered when events has loaded, and the button also needs to show a count of how many events there is.
So my question is how to pass events data up from QueryRenderer (popover) to a parent component (toggle button)?
My first idea was to reuse my QueryRenderer for events and pass in dataFrom={'STORE_ONLY'}, to avoid a new HTTP request and use the cache instead, but unfortunately 'STORE_ONLY' is not an option... YET...
From looking at https://github.com/relay-tools/relay-hooks/issues/5 it seems like store-only will be supported by useQuery in the future, so is that the recommended solution to go about it, or how is the recommended way? Surely facebook, and many other applications, must have had this need frequently?
You can achieve redux-like relay store with custom handlers and local schema.
I'll be guessing what your queries, components and fields might be named like so don't forget to change it to correct values
Somewhere in project's src folder create a file ClientState.client.graphql to extend your root query type with new field for client state:
// ClientState.client.graphql
type ClientState {
showToggleButton: Boolean!
eventsCount: Int
}
extend type Query {
clientState: ClientState!
}
this will allow you to wrap Toggle button with fragment like this:
fragment ToggleButton_query on Query {
clientState {
showToggleButton
eventsCount
}
}
and spread this fragment in parent query (probably AppQuery)
Then in your second query, where you'll be fetching events, add #__clientField directive, to define custom handle for that field:
query EventModal {
events #__clientField(handle: "eventsData") {
totalCount
}
}
Create EventsDataHandler for handle eventsData:
// EventsDataHandler.js
// update method will be called every time when field with `#__clientField(handle: "eventsData")` is fetched
const EventsDataHandler = {
update (store, payload) {
const record = store.get(payload.dataID)
if (!record) {
return
}
// get "events" from record
const events = record.getLinkedRecord(payload.fieldKey)
// get events count and set client state values
const eventsCount = events.getValue('totalCount')
const clientState = store.getRoot().getLinkedRecord('clientState')
clientState.setValue(eventsCount, 'eventsCount')
clientState.setValue(true, 'showToggleButton')
// link "events" to record, so the "events" field in EventModal is not undefined
record.setLinkedRecord(events, payload.handleKey)
}
}
export default EventsDataHandler
Last thing to do is to assign custom (and default) handlers to environment and create init store values:
// environment.js
import { commitLocalUpdate, ConnectionHandler, Environment, RecordSource, Store, ViewerHandler } from 'relay-runtime'
import EventsDataHandler from './EventsDataHandler'
// ...
const handlerProvider = handle => {
switch (handle) {
case 'connection':
return ConnectionHandler
case 'viewer':
return ViewerHandler
case 'eventsData':
return EventsDataHandler
default:
throw new Error(`Handler for ${handle} not found.`)
}
}
const environment = new Environment({
network,
store,
handlerProvider
})
// set init client state values
commitLocalUpdate(environment, store => {
const FIELD_KEY = 'clientState'
const TYPENAME = 'ClientState'
const dataID = `client:${FIELD_KEY}`
const record = store.create(dataID, TYPENAME)
record.setValue(false, 'showToggleButton')
// prevent relay from removing client state
environment.retain({
dataID,
variables: {},
node: { selections: [] }
})
store.getRoot().setLinkedRecord(record, FIELD_KEY)
})

Create a new stream from a stream in Dart

I suspect that my understanding of Streams in Dart might have a few holes in it...
I have a situation in which I'd like a Dart app to respond to intermittent input (which immediately suggests the use of Streamss -- or Futures, maybe). I can implement the behavior I want with listener functions but I was wondering how to do this in a better, more Dartesque way.
As a simple example, the following (working) program listens to keyboard input from the user and adds a div element to the document containing what has been typed since the previous space, whenever the space bar is hit.
import 'dart:html';
main() {
listenForSpaces(showInput);
}
void listenForSpaces(void Function(String) listener) {
var input = List<String>();
document.onKeyDown.listen((keyboardEvent) {
var key = keyboardEvent.key;
if (key == " ") {
listener(input.join());
input.clear();
} else {
input.add(key.length > 1 ? "[$key]" : key);
}
});
}
void showInput(String message) {
document.body.children.add(DivElement()..text = message);
}
What I'd like to be able to do is to create a new Stream from the Stream that I'm listening to (in the example above, to create a new Stream from onKeyDown). In other words, I might set the program above out as:
var myStream = ...
myStream.listen(showInput);
I suspect that there is a way to create a Stream and then, at different times and places, insert elements to it or call for it to emit a value: it feels as though I am missing something simple. In any case, any help or direction to documentation would be appreciated.
Creating a new stream from an existing stream is fairly easy with an async* function.
For a normal stream, I would just do:
Stream<String> listenForSpaces() async* {
var input = <String>[];
await for (var keyboardEvent in document.onKeyDown) {
var key = keyboardEvent.key;
if (key == " ") {
yield input.join();
input.clear();
} else {
input.add(key.length > 1 ? "[$key]" : key);
}
}
}
The async* function will propagate pauses through to the underlying stream, and it may potentially pause the source during the yield.
That may or may not be what you want, since pausing a DOM event stream can cause you to miss events. For a DOM stream, I'd probably prefer to go with the StreamController based solution above.
There are several methods and there is a whole package rxdart to allow all kinds of things.
Only the final consumer should use listen and only if you need to explicitly want to unsubscribe, otherwise use forEach
If you want to manipulate events like in your example, use map.
I wasn't originally planning to answer my own question but I have since found a very simple answer to this question in the dartlang creating streams article; in case it's helpful to others:
Specifically, if we'd like to create a stream that we can insert elements into at arbitrary times and places in the code, we can do so via the StreamController class. Instances of this class have an add method; we can simply use the instance's stream property as our stream.
As an example, the code in my question could be rewritten as:
import 'dart:html';
import 'dart:async';
main() async {
// The desired implementation stated in the question:
var myStream = listenForSpaces();
myStream.listen(showInput);
}
Stream<String> listenForSpaces() {
// Use the StreamController class.
var controller = StreamController<String>();
var input = List<String>();
document.onKeyDown.listen((keyboardEvent) {
var key = keyboardEvent.key;
if (key == " ") {
// Add items to the controller's stream.
controller.add(input.join());
input.clear();
} else {
input.add(key.length > 1 ? "[$key]" : key);
}
});
// Listen to the controller's stream.
return controller.stream;
}
void showInput(String message) {
document.body.children.add(DivElement()..text = message);
}
(As mentioned in the article, we need to be careful if we want to set up a stream from scratch like this because there is nothing to stop us from inserting items to streams that don't have associated, active subscribers; inserted items would in that case be buffered, which could result in a memory leak.)

How do I bind to Spinner.SelectedItem

I would like to bind the selected text from a spinner to a string named SelectedRole in my ViewModel. This is what I did
this.Bind(ViewModel, vm => vm.SelectedRole, v => v.roleSpinner.SelectedItem.ToString());
However, I ran into an exception.
System.NotSupportedException: Index expressions are only supported with constants.
The SelectedItem property of Spinner has read-only access (no setter), so Bind won't work since it's two-way.
One alternative is to install the ReactiveUI.Events package and use the ItemSelected observable like this:
_spinner.Events().ItemSelected
.Select(_ => _spinner.SelectedItem.ToString())
.BindTo(ViewModel, x => x.Selected);
and of course the view model property is reactive:
private string _selected;
public string Selected
{
get => _selected;
set => this.RaiseAndSetIfChanged(ref _selected, value);
}
and if you want to initialize the spinner value, use the SetSelection method:
_spinner.SetSelection(2);
I tested all this on my phone and it works as expected. Let me know if this functionality fits your needs.

How do I make View's asList() sortable in Google Dataflow SDK?

We have a problem making asList() method sortable.
We thought we could do this by just extending the View class and override the asList method but realized that View class has a private constructor so we could not do this.
Our other attempt was to fork the Google Dataflow code on github and modify the PCollectionViews class to return a sorted list be using the Collections.sort method as shown in the code snippet below
#Override
protected List<T> fromElements(Iterable<WindowedValue<T>> contents) {
Iterable<T> itr = Iterables.transform(
contents,
new Function<WindowedValue<T>, T>() {
#SuppressWarnings("unchecked")
#Override
public T apply(WindowedValue<T> input){
return input.getValue();
}
});
LOG.info("#### About to start sorting the list !");
List<T> tempList = new ArrayList<T>();
for (T element : itr) {
tempList.add(element);
};
Collections.sort((List<? extends Comparable>) tempList);
LOG.info("##### List should now be sorted !");
return ImmutableList.copyOf(tempList);
}
Note that we are now sorting the list.
This seemed to work, when run with the DirectPipelineRunner but when we tried the BlockingDataflowPipelineRunner, it didn't seem like the code change was being executed.
Note: We actually recompiled the dataflow used it in our project but this did not work.
How can we be able to achieve this (as sorted list from the asList method call)?
The classes in PCollectionViews are not intended for extension. Only the primitive view types provided by View.asSingleton, View.asSingleton View.asIterable, View.asMap, and View.asMultimap are supported.
To obtain a sorted list from a PCollectionView, you'll need to sort it after you have read it. The following code demonstrates the pattern.
// Assume you have some PCollection
PCollection<MyComparable> myPC = ...;
// Prepare it for side input as a list
final PCollectionView<List<MyComparable> myView = myPC.apply(View.asList());
// Side input the list and sort it
someOtherValue.apply(
ParDo.withSideInputs(myView).of(
new DoFn<A, B>() {
#Override
public void processElement(ProcessContext ctx) {
List<MyComparable> tempList =
Lists.newArrayList(ctx.sideInput(myView));
Collections.sort(tempList);
// do whatever you want with sorted list
}
}));
Of course, you may not want to sort it repeatedly, depending on the cost of sorting vs the cost of materializing it as a new PCollection, so you can output this value and read it as a new side input without difficulty:
// Side input the list, sort it, and put it in a PCollection
PCollection<List<MyComparable>> sortedSingleton = Create.<Void>of(null).apply(
ParDo.withSideInputs(myView).of(
new DoFn<Void, B>() {
#Override
public void processElement(ProcessContext ctx) {
List<MyComparable> tempList =
Lists.newArrayList(ctx.sideInput(myView));
Collections.sort(tempList);
ctx.output(tempList);
}
}));
// Prepare it for side input as a list
final PCollectionView<List<MyComparable>> sortedView =
sortedSingleton.apply(View.asSingleton());
someOtherValue.apply(
ParDo.withSideInputs(sortedView).of(
new DoFn<A, B>() {
#Override
public void processElement(ProcessContext ctx) {
... ctx.sideInput(sortedView) ...
// do whatever you want with sorted list
}
}));
You may also be interested in the unsupported sorter contrib module for doing larger sorts using both memory and local disk.
We tried to do it the way Ken Knowles suggested. There's a problem for large datasets. If the tempList is large (so sort takes some measurable time as it's proportion to O(n * log n)) and if there are millions of elements in the "someOtherValue" PCollection, then we are unecessarily re-sorting the same list millions of times. We should be able to sort ONCE and FIRST, before passing the list to the someOtherValue.apply's DoFn.

Loading parent and child nodes from remote data

I've got a method in my controller that returns a List<TreeViewItemModel>() that I'm populating with the correct hierarchy. This seems to serialize correctly, but when I load the Treeview, I don't have any of the hierarchy, just the first level of nodes.
Example:
Each of the above Curricula has 2/3 scenarios underneath that I've verified are getting added as items to the base object when going from curriculum => TreeViewItemModel
Controller:
public JsonResult GetAvailableCurricula(string LocationId)
{
LocationId = "1";
if(LocationId != string.Empty)
{
var results = Logic.GetFilteredCurriculum().Select(c => CurriculumToTreeView(c));
return Json(results, JsonRequestBehavior.AllowGet);
}
else
{
return Json(new List<TreeViewItemModel>(),JsonRequestBehavior.AllowGet);
}
}
private TreeViewItemModel CurriculumToTreeView(CurriculumModel c)
{
var tree = new TreeViewItemModel()
{
Id = c.CurriculumId.ToString(),
Text = c.CurriculumName,
HasChildren = c.Scenarios.Any()
};
if (tree.HasChildren)
{
tree.Items = c.Scenarios.Select(scenario =>
new TreeViewItemModel()
{
Text = scenario.Name,
}
).ToList();
}
return tree;
}
View:
#(Html.Kendo().TreeView()
.Name("AvailableCurricula")
.DataTextField("Text")
.DataSource(source => source
.Read(read => read
.Action("GetAvailableCurricula", "TraineeAssignments")
.Data("filterAvailableCurricula")
)
)
Is there some extra step I need to take to bind the child objects AND the parent, instead of just one level at a time? I have a fairly small set of data that I don't need to reload all that often, so I would was hoping to avoid loading each level individually/on-demand.
In case it's helpful, here's the raw JSON I'm sending from the controller for one of my curricula:
{"Enabled":true,"Expanded":false,"Encoded":true,"Selected":false,"Text":"Operator B","SpriteCssClass":null,"Id":"1","Url":null,"ImageUrl":null,"HasChildren":true,"Checked":false,"Items":[{"Enabled":true,"Expanded":false,"Encoded":true,"Selected":false,"Text":"test 2","SpriteCssClass":null,"Id":null,"Url":null,"ImageUrl":null,"HasChildren":false,"Checked":false,"Items":[],"HtmlAttributes":{},"ImageHtmlAttributes":{},"LinkHtmlAttributes":{}},{"Enabled":true,"Expanded":false,"Encoded":true,"Selected":false,"Text":"Scenario II","SpriteCssClass":null,"Id":null,"Url":null,"ImageUrl":null,"HasChildren":false,"Checked":false,"Items":[],"HtmlAttributes":{},"ImageHtmlAttributes":{},"LinkHtmlAttributes":{}}],"HtmlAttributes":{},"ImageHtmlAttributes":{},"LinkHtmlAttributes":{}}
I believe that the remote data option for the Treeview uses ajax to load the child data, either on demand or at initial load - controlled by the LoadOnDemand option.
The remote data examples in the kendo docs behave that way. That is how I implemented it on a previous project. The full code example includes a treeview as well as a grid.

Resources