Unsorted keys in note will be sorted - vexflow

I'm creating a stave note with multiple keys:
const staveNote: vexflow.Flow.StaveNote = new this.VF.StaveNote({
keys: this.renderNotesSortedByPitch(placedChord.notes),
duration: chordDuration,
auto_stem: true,
clef: Clef.TREBLE
});
private renderNotesSortedByPitch(notes: Array<Note>): Array<string> {
const vexflowNotes: Array<string> = new Array<string>();
notes
// this.sortNotesByPitch(notes)
.forEach((note: Note) => {
vexflowNotes.push(this.renderNote(note));
});
return vexflowNotes;
}
private sortNotesByPitch(notes: Array<Note>): Array<Note> {
return notes.sort((noteA: Note, noteB: Note) => {
return noteA.pitch.chroma.value - noteB.pitch.chroma.value <--- No arithmetic operation on strings
});
}
and I get the following warning in the browser console:
Warning: Unsorted keys in note will be sorted. See https://github.com/0xfe/vexflow/issues/104 for details. Error
at Function.b.StackTrace (http://localhost:4200/vendor.js:93990:4976)
at Function.b.W (http://localhost:4200/vendor.js:93990:5134)
at http://localhost:4200/vendor.js:93990:255605
at Array.forEach (<anonymous>)
at e.value (http://localhost:4200/vendor.js:93990:255572)
at new e (http://localhost:4200/vendor.js:93990:250357)
at SheetService.vexflowRenderSoundtrack (http://localhost:4200/main.js:2083:51)
at SheetService.createSoundtrackSheet (http://localhost:4200/main.js:2004:14)
at SheetComponent.createSheet (http://localhost:4200/main.js:2465:35)
at SheetComponent.ngAfterViewInit (http://localhost:4200/main.js:2452:14)
I understand I need to provide the keys already sorted the way Vexflow is sorting them.
A similar issue is also described there.
How to sort the keys with the note.pitch.chroma.value being a string ?
It'd be nice to have some method in the same fashion as:
staveNote.setKeyStyle(0, { fillStyle: 'red' });
Say, some such method:
staveNote.setDotted(0);
Or:
staveNote.setKeyStyle(0, { fillStyle: 'red', dotted: true });
UPDATE: Following a suggestion I could create the methods to sort the notes before adding them as keys in the stave:
private getNoteFrequency(note: Note): number {
return Tone.Frequency(note.renderAbc()).toFrequency();
}
private sortNotesByPitch(notes: Array<Note>): Array<Note> {
return notes.sort((noteA: Note, noteB: Note) => {
return this.getNoteFrequency(noteA) - this.getNoteFrequency(noteB);
});
}
The Vexflow warning message was no longer displayed in the browser console.

Vexflow expects your notes to be sorted vertically, no way around that.
You need to write your own function to compare two notes given as strings.
here's a working note-string-comparison-function which doesn't take accidentals into account: repl.it/repls/WobblyFavorableYottabyte
edited for clarity, thanks #gristow for the correction!

Related

Angular Material Data Table - How To Setup filterPredicate For A Column With Type Ahead / Auto Complete Search?

I've read the various implementations of filterPredicate on SO, Github, etc but they aren't helpful for me to understand what to do with type ahead searches.
I enter a letter into an input form field, say p, and I receive all the data with last names starting with p from the db. That part of my setup works fine. However, I don't want to hit the db again when I type the next letter, say r. I want to filter the data table for last names starting with pr. This is where the trouble starts.
When I type the second letter I have an if/else statement that tests if the var I'm using has >1 in the string. When it does I pass params to a function for the custom filtering on the table with the data already downloaded from the db. I'm avoiding a db call with every letter, which does work. I don't understand "(data, filter)". They seem like params but aren't. How do they work? What code is needed to finish this?
(I have `dataSource.filter = filterValue; working fine elsewhere.)
Params explained:
column = user_name
filterValue = pr...
The confusion:
public filterColumn(column: string, filterValue: string, dataSource) {
dataSource.filterPredicate = (data, filter) => {
console.log('data in filter column: ', data); // Never called.
// What goes here?
// return ???;
}
}
My dataSource object. I see filterPredicate, data, and filter properties to work with. Rather abstract how to use them.
dataSource in filterColumn: MatTableDataSource {_renderData: BehaviorSubject, _filter: BehaviorSubject, _internalPageChanges: Subject, _renderChangesSubscription: Subscriber, sortingDataAccessor: ƒ, …}
filterPredicate: (data, filter) => {…}arguments: [Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
at Function.invokeGetter (<anonymous>:2:14)]caller: (...)length: 2name: ""__proto__: ƒ ()[[FunctionLocation]]: data-utilities.service.ts:43[[Scopes]]: Scopes[3]
filteredData: (3) [{…}, {…}, {…}]
sortData: (data, sort) => {…}
sortingDataAccessor: (data, sortHeaderId) => {…}
_data: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_filter: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_internalPageChanges: Subject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}
_paginator: MatPaginator {_isInitialized: true, _pendingSubscribers: null, initialized: Observable, _disabled: false, _intl: MatPaginatorIntl, …}
_renderChangesSubscription: Subscriber {closed: false, _parentOrParents: null, _subscriptions: Array(1), syncErrorValue: null, syncErrorThrown: false, …}
_renderData: BehaviorSubject {_isScalar: false, observers: Array(1), closed: false, isStopped: false, hasError: false, …}data: (...)filter: (...)paginator: (...)sort: (...)__proto__: DataSource
I've included most of the component I made in Angular for typeahead search. The guts of the typeahead code is in the utilities shared component at the bottom. I used a shared component here because I'll use this in many places. However, I think it is a hack and a more elegant answer is possible. This works, it is easy, but not all that pretty. I can't afford more time to figure out pretty now. I suspect the answer is in RegEx.
In the typeahead.compoent in the .pipe you'll find how I call the code in the utility.
This code is in a shared component typeahead.component.ts
public searchLastName$ = new Subject<string>(); // Binds to the html text box element.
ngAfterViewInit() {
// -------- For Column Incremental Queries --------- //
// searchLastName$ binds to the html element.
this.searchLastName$.subscribe(result => {
this.queryLastName(result);
});
}
// --------- LAST NAME INCREMENTAL QUERY --------------- //
private queryLastName(filterValue) {
// Custom filter for this function. If in ngOnInit on the calling component then it applies
// to the whole calling component. We need various filters so that doesn't work.
this.membersComponent.dataSource.filterPredicate = (data: { last_name: string }, filterValue: string) =>
data.last_name.trim().toLowerCase().indexOf(filterValue) !== -1;
// When the first letter is typed then get data from db. After that just filter the table.
if (filterValue.length === 1) {
filterValue = filterValue.trim(); // Remove whitespace
// filterValue = filterValue.toUpperCase(); // MatTableDataSource defaults to lowercase matches
const lastNameSearch = gql`
query ($last_name: String!) {
lastNameSearch(last_name: $last_name) {
...membersTableFrag
}
}
${membersTableFrag}
`;
this.apollo
.watchQuery({
query: lastNameSearch,
variables: {
last_name: filterValue,
},
})
.valueChanges
.pipe(
map(returnedArray => {
// console.log('returnedArray in map: ', returnedArray); // All last_name's with the letter in them someplace.
const membersArray = returnedArray.data['lastNameSearch']; // extract items array from GraphQL JSON array
// For case insensitive search
const newArray = membersArray.filter(this.utilitiesService.filterBy(filterValue, 'last_name'));
return newArray;
})
)
.subscribe(result => {
this.membersComponent.dataSource.data = result;
});
} else {
// Filter the table instead of calling the db for each letter entered.
// Note: Apollo client doesn't seem able to query the cache with this kind of search.
filterValue = filterValue.trim(); // Remove whitespace
filterValue = filterValue.toLowerCase(); // MatTableDataSource defaults to lowercase matches
// Interface and redefinition of filterPredicate in the ngOnInit
this.membersComponent.dataSource.filter = filterValue; // Filters all columns unless modifed by filterPredicate.
}
}
utilities.service.ts
// -------------- DATABASE COLUMN SEARCH -------------
// Shared with other components with tables.
// For case insensitive search.
// THIS NEEDS TO BE CLEANED UP BUT I'M MOVING ON, MAYBE LATER
public filterBy = (filterValue, column) => {
return (item) => {
const charTest = item[column].charAt(0);
if (charTest === filterValue.toLowerCase()) {
return true;
} else if (charTest === filterValue.toUpperCase()) {
return true;
} else {
return false;
}
}
};

How to make angular2 custom validators to run after we get the value

If I pass hard coded values in offerCheck validator it is working fine. But if I get values from api, null values is getting passed in paramets. Form is getting executed before we get the values from service. Please help me to make validate check after getting values from api.
this.newOffer = "aaa";
this.oldOffer = "aaa";
constructor(fb: FormBuilder) {
this.formGroup = fb.group({
'offer': [null, Validators.compose([Validators.required, this.offerCheck(newOfer, oldOffer)])],
})
offerCheck(new, old) {
return (control: FormControl) => {
if (new == old) {
return true;
}
}
}
What you want is probably the AsyncValidatorFn, here's a very simple example of how to create one:
export const OfferCheck: AsyncValidatorFn = (control: AbstractControl): Observable<boolean> => {
if (new == old) {
return Observable.of(this.http.get('/some-endpoint').first().map(res => res.data));
}
};
You don't provide enough information so this is just a guess on how you'd want it to be. But it should point you in the right decision.
An alternative method would be to use setValidators of the control(s) after you've fetched the data:
this.formGroup.get('offer').setValidators([Validators.required, this.offerCheck(newOfer, oldOffer)]);
I hope this helps.

How to update connection metadata in client-side store?

I'm attempting to learn Relay by implementing TodoMVC from scratch.
I can query my data like this which is working well:
query {
allTodos(first: 100) {
totalCount
completedCount
edges {
node {
id
text
completed
}
}
}
}
I got the idea to add the totalCount and completedCount metadata to the connection from here: http://graphql.org/learn/pagination/#end-of-list-counts-and-connections
It's similar in this example: https://github.com/graphql/swapi-graphql/blob/master/src/schema/index.js#L78
Now I am writing a mutation to change the completed field of a Todo given its id.
I gather I will need to return the new completedCount in the mutation payload, but I'm not sure how to implement getConfigs() to update this in the client-side store. I don't have an id for the connection, right? Is there is a flaw in my schema design? Thanks!
Assuming your mutation returns a viewer, you'll need to add the viewer to your fatQuery and getConfigs. I think this tutorial might be helpful. Here's the excerpt relevant to your task:
Adding a Todo is more complex. The reason for this is that we need to
update not only the state of a Todo object that we will create, but
also a connection where it is stored - the count of Todos will change,
as well as the listing of Todo nodes in edges.
import Relay from 'react-relay';
export default class AddTodoMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`fragment on ReindexViewer {
id
allTodos {
count,
}
}`
};
getMutation() {
return Relay.QL`mutation{ createTodo }`;
}
getVariables() {
return {
text: this.props.text,
complete: false,
};
}
getFatQuery() {
return Relay.QL`
fragment on _TodoPayload {
changedTodoEdge,
viewer {
id,
allTodos {
count
}
}
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentID: this.props.viewer.id,
connectionName: 'allTodos',
edgeName: 'changedTodoEdge',
rangeBehaviors: {
'': 'prepend',
},
}, {
type: 'FIELDS_CHANGE',
fieldIDs: {
viewer: this.props.viewer.id,
},
}];
}
getOptimisticResponse() {
return {
changedTodoEdge: {
node: {
text: this.props.text,
complete: false,
},
},
viewer: {
id: this.props.viewer.id,
allTodos: {
count: this.props.viewer.allTodos.count + 1,
},
},
};
}
}
In order to perform this mutation, we need some data that might not be
available to the component - the id of viewer object and count of
allTodos connection. Therefore we need to specify fragments for the
mutation same way as we specify them for containers.
Our configs are more complex this time too - we need to add our new
Todo to a connection, so we use RANGE_ADD mutation config. Relay
expects an edge to be passed in payload, not just a Todo, Reindex
provides changedTodoEdge for this. Lastly we need to fetch updated
connection count from the server and for this viewer field is
available for every payload.
In our optimistic update we increment the count of allTodos, so that
we change our “total” display without any delay.

Invalidate Falcor jsonGraph fragment using jsonGraphEnvelope

I'm trying to invalidate a part of my jsonGraph object via the response from the falcor-router after making a CREATE call. I can successfully do so when returning a list of pathValues, similar to this earlier SE question:
{
route: 'foldersById[{keys:ids}].folders.createSubFolder',
call(callPath, args, refPaths, thisPaths) {
return createNewFolderSomehow(...)
.subscribe(folder => {
const folderPathValue = {
path: ['foldersById', folder.parentId, 'folders', folder.parentSubFolderCount -1],
value: $ref(['foldersById', folder.id])
};
const folderCollectionLengthPathValue = {
path: ['folderList', 'length'],
invalidated: true
};
return [folderPathValue, folderCollectionLengthPathValue];
});
})
}
However, when returning the equivalent (afaik) jsonGraphEnvelope, the invalidated path is dropped from the response:
{
route: 'foldersById[{keys:ids}].folders.createSubFolder',
call(callPath, args, refPaths, thisPaths) {
return createNewFolderSomehow(...)
.subscribe(folder => {
const newFolderPath = ['foldersById', folder.parentId, 'folders', folder.parentSubFolderCount -1];
return {
jsonGraph: R.assocPath(folderPath, $ref(['foldersById', folder.id]), {})
paths: [newFolderPath],
invalidated: [['folderList', 'length']]
};
});
})
}
Am I misunderstanding how a jsonGraphEnvelope works (had assumed it was a longhand format equivalent to an array of PathValues)? Or is this likely a bug?
Looks like a bug to me.
Invalidations don't seem to be handled in the part of the code responsible for merging partial JSONGraph envelopes returned from routes into the JSONGraph envelope response (see here), while they are handled in the path-value merge (see here).
I can't find any issue about this on GitHub so I invite you to open one.

AngularFire: How to update a $firebaseArray by extending the service and using the $$updated private method

I have two separate lists of Posts and Authors in my database, each Post containing an authorId to refer to the corresponding author.
The following method helps me retrieve the whole list of Posts by systematically including the name of the author for each Post:
app.factory('NormalizedPosts', function($firebaseArray, FirebaseFactory) {
var PostsWithAuthors = $firebaseArray.$extend({
// override $$added to include author name
$$added: function(snap) {
var record = $firebaseArray.prototype.$$added.call(this, snap);
FirebaseFactory.$getAuthorId( record.authorId ).$loaded(function( authorData ) {
record.authorData = authorData;
});
return record;
},
// ????????
$$updated: function(snap) {
var rec = $firebaseArray.prototype.$$updated.call(this, snap);
var updatedRecord = this.$getRecord(snap.key());
FirebaseFactory.$getAuthorId( updatedRecord.authorId )
.$loaded(function( authorData ) {
rec.authorData = authorData;
});
return rec;
}
});
return PostsWithAuthors;
});
PS: The FirebaseFactory is just a wrapper for firebase methods.
I then call
var list = new NormalizedPosts ( new Firebase(FBURL).child("posts") );
in my controller to get the full list. This works great.
I'm scratching my head with what should go into the $$updated method: When a new Post is added, the list gets updated as expected (through the $$added method). But when there's a change in a Post data (e.g. the post title), my list does not get updated, as I'm currently returning false in the $$updated method.
Question: What should go in the $$updated method so that when theres a change in a Post data, my list gets updated accordingly (and further returns the author's name!). Thanks
I think you're going through too many loops to get something simple done.
If the name of the author is "part of" the article than you should save it:
{
"articles": {
"firebase-uniq-id-1": {
"title": "My Writing Process",
"published": "2016-01-01",
"author": {
"firebase-uniq-id-2": "Ernest Hemingway"
}
}
},
"authors": {
"firebase-uniq-id-2": {
"name": "Ernest Hemingway",
"born": "1899-07-21",
"whatever": "Some Data"
}
}
}
When you'll want to show other details about the author, fetch it.
Edit:
If you still wish to use the extension option, I believe the only thing you're missing there is extending the updateRecord with rec:
FirebaseFactory.$getAuthorId( updatedRecord.authorId )
.$loaded(function( authorData ) {
updatedRecord.authorData = authorData;
angular.extend(updatedRecord, snap.val());
});
Also, in $$updated you need to return a boolean saying if the record changed or not, and not the record itself.
Keep in mind that if you go that path you shouldn't use the default $save() method of $firebaseArray since it will also save the full authorData
$$updated: function(snap) {
// boolean for the $$updated method
var rec = $firebaseArray.prototype.$$updated.call(this, snap);
// existing record as per this
var existingRecord = this.$getRecord(snap.key());
// record as per FB database
var updatedRecord = snap.val();
if ( rec ) {
FirebaseFactory.$getAuthorId( updatedRecord.authorId )
.$loaded(function( authorData ) {
updatedRecord.authorData = authorData;
angular.extend(existingRecord, updatedRecord);
});
} // end if loop
return rec;
}

Resources