watch expression is not picking up on model change from ng-change - ruby-on-rails

I have the following
Rails HAML:
= select_tag "some-class",
options_for_select([['None', '']], ''),
{ class: 'some-other-class',
'ng-model' => 'someModel',
'ng-options' => 'option.name for option in someList',
'ng-change' => 'updateSelected()'}
Angular Controller:
scope.updateSelected = ->
#logic for updating model lives here. Model updates successfully by using some values defined within scope. Includes the following:
scope.someModel = "some_new_value"
Angular Directive:
SomeClassDirective= ->
restrict: 'C'
link: (scope, element, attrs) ->
monitorFormFields = (newValue, oldValue) ->
console.log "this is the inner function call"
#logic for setting the inner _destroy field lives here
scope.$watch 'someModel', monitorFormFields
However, when the Select List value is changed, 'this is the inner function call' never prints.(it does print when the directive first initializes, ie at page load). My question therefore is: Why isn't the $watch expression triggering, and how do I get it to trigger?
Thanks!

With this HTML:
<select class="some-class" ng-model="someModel"
ng-options="option.name for option in someList"></select>
Here is a directive that will watch for a change to someModel:
myApp.directive('someClass', function () {
return {
restrict: 'C',
link: function (scope, element, attrs) {
var monitorFormFields = function (newValue, oldValue) {
console.log("this is in the inner function call");
}
scope.$watch('someModel', monitorFormFields);
}
}
});
Controller:
$scope.someList = [{ name: 'name1' }, { name: 'name2' }];
Note that you don't need to call a controller method to update someModel -- Angular does that automatically for us because of the ng-model attribute. So, the directive only needs to $watch for a change to that $scope property.
Fiddle.
I would like to from the element fetch a sibling with [_destroy] in the name and set it to either "0" or "1" depending on the value of the select box.
A more Angular approach would be to have model properties control whether "0" or "1" is displayed. E.g., in your controller:
$scope.destroy1 = "0";
$scope.destroy2 = "0";
In your HTML:
<div>{{destroy1}}</div>
<div>{{destroy2}}</div>
In monitorFormFields() you can change the values of these scope properties, and the view will automatically update -- there is no need to "find" siblings or update .val()ues.

Related

Need assistance reading the object returned by getRowId of MaterialReactTable

I am using MaterialReactTable in my application and following the Row Selection Option as outlined at this link: https://www.material-react-table.com/docs/guides/row-selection
The table is working fine and I am able to select the row I want and it returns the correct id but returns it in the format: rowSelection = {63d19bebc764a5587a48683a: true}. I am not familiar with this format.
I have tried everything I know but am unable to parse out the id from the object.
Please provide suggestion to parse out the id or suggest changes to make this solution work.
I have tried the other methods of row selection suggested on the page (useRef and '#tanstack/react-table') and could not get either to work so would like to stick to this method as I feel it is close.
Below is the code and options I am using with the MaterialReactTable
return (
<MaterialReactTable
columns={columns}
data={data}
enableRowSelection
onRowSelectionChange={setRowSelection}
enableMultiRowSelection={false}
//getRowId={(row) => row?._id }
getRowId={(originalRow) => originalRow._id}
initialState={{ showColumnFilters: true,
columnVisibility:
{ _id: false } }} //hide columns listed to start }}
manualFiltering
manualPagination
manualSorting
muiToolbarAlertBannerProps={
isError
? {
color: 'error',
children: 'Error loading data',
}
: undefined
}
muiTableBodyRowProps={({ row }) => ({
//add onClick to row to select upon clicking anywhere in the row
onClick: row.getToggleSelectedHandler(),
sx: { cursor: 'pointer' },
})}
onColumnFiltersChange={setColumnFilters}
onGlobalFilterChange={setGlobalFilter}
onPaginationChange={setPagination}
onSortingChange={setSorting}
rowCount={rowCount}
state={{
columnFilters,
globalFilter,
isLoading,
pagination,
showAlertBanner: isError,
showProgressBars: isRefetching,
sorting,
rowSelection
}}
/>
);
Given the format of the response, rowSelection = {63d19bebc764a5587a48683a: true}, I had originally assumed a key: value pair with the id being the key. My initial attempts to parse out the id as the key had failed. After trying a number of different options, I was able to use the Object.keys() function as follows:
console.log(Object.keys(rowSelection)); //used to view the key(s) returned
setCurrentRoom(Object.keys(rowSelection));
This code converted the id to a string in an array as follows: currentRoom = ['63d19bd9c764a5587a486836']

How do you add an initial selection for Angular Material Table SelectionModel?

The Angular Material documentation gives a nice example for how to add selection to a table (Table Selection docs). They even provide a Stackblitz to try it out.
I found in the code for the SelectionModel constructor that the first argument is whether there can be multiple selections made (true) or not (false). The second argument is an array of initially selected values.
In the demo, they don't have any initially selected values, so the second argument in their constructor (line 36) is an empty array ([]).
I want to change it so that there is an initially selected value, so I changed line 36 to:
selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);
This changes the checkbox in the header to an indeterminate state (as expected), but does not cause the row in the table to be selected. Am I setting the initial value incorrectly, or what am I missing here? How can I set an initially selected value?
Tricky one. You need to initialize the selection by extracting that particular PeriodicElement object from your dataSource input, and passing it to the constructor.
In this particular case, you could code
selection = new SelectionModel<PeriodicElement>(true, [this.dataSource.data[1]);
It's because of the way SelectionModel checks for active selections.
In your table markup you have
<mat-checkbox ... [checked]="selection.isSelected(row)"></mat-checkbox>
You expect this binding to mark the corresponding row as checked. But the method isSelected(row) won't recognize the object passed in here as being selected, because this is not the object your selection received in its constructor.
"row" points to an object from the actual MatTableDataSource input:
dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
But the selection initialization:
selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);
happens with a new object you create on the fly. Your selection remembers THIS object as a selected one.
When angular evaluates the bindings in the markup, SelectionModel internally checks for object identity. It's going to look for the object that "row" points to in the internal set of selected objects.
Compare to lines 99-101 and 16 from the SelectionModel source code:
isSelected(value: T): boolean {
return this._selection.has(value);
}
and
private _selection = new Set<T>();
I was facing the same issue, I used dataSource to set the initial value manually in ngOnInit()
ngOnInit() {
this.dataSource.data.forEach(row => {
if (row.symbol == "H") this.selection.select(row);
});
}
If you do the following, it works too
selection = new SelectionModel<PeriodicElement>(true, [ELEMENT_DATA[1]])
To select all you can do
selection = new SelectionModel<PeriodicElement>(true, [...ELEMENT_DATA])
I hope the answer is helpful
Or more dynamically if you have a set of values and you want to filter them before:
selection = new SelectionModel<PeriodicElement>(true, [
...this.dataSource.data.filter(row => row.weight >= 4.0026)
]);
This gets more tricky if you have data loading asynchronously from an api. Here is how I did it:
Firstly I have implemented the DataSource from "#angular/cdk/table". I also have an RxJS Subject that fires whenever data is loaded (first time or when user changes page in the pagination section)
export abstract class BaseTableDataSource<T> implements DataSource<T>{
private dataSubject = new BehaviorSubject<T[]>([]);
private loadingSubject = new BehaviorSubject<boolean>(false);
private totalRecordsSubject = new BehaviorSubject<number>(null);
public loading$ = this.loadingSubject.asObservable();
public dataLoaded$ = this.dataSubject.asObservable();
public totalRecords$ = this.totalRecordsSubject.asObservable().pipe(filter(v => v != null));
constructor(){}
connect(collectionViewer: CollectionViewer): Observable<T[]>{
return this.dataSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.dataSubject.complete();
this.loadingSubject.complete();
this.totalRecordsSubject.complete();
}
abstract fetchData(pageIndex, pageSize, ...params:any[]) : Observable<TableData<T>>;
abstract columnMetadata(): {[colName: string]: ColMetadataDescriptor };
loadData(pageIndex, pageSize, params?:any[]): void{
this.loadingSubject.next(true);
this.fetchData(pageIndex, pageSize, params).pipe(
finalize(() => this.loadingSubject.next(false))
)
.subscribe(data => {
this.totalRecordsSubject.next(data.totalNumberOfRecords);
this.dataSubject.next(data.records)
});
}
}
Now when I want to pre-select a row, I can write a function like this in my component which hosts a table that uses an implementation of the above mentioned data source
selectRow(rowSelectionFn: (key: string) => boolean){
this.dataSource.dataLoaded$.pipe(takeUntil(this.destroyed$))
.subscribe(data => {
const foundRecord = data.filter(rec => rowSelectionFn(rec));
if(foundRecord && foundRecord.length >= 0){
this.selection.toggle(foundRecord[0]);
}
});
}

Convert Select2 input to tokens

Does the Select2 jQuery plug-in have a built-in function for converting strings to tokens?
I want to be able to call this tokenizing function when the user pastes strings into a Select2 field so that the pasted input becomes tokens.
I think I have solved the question myself with the following code:
// force tokenizing of Select2 auto-complete fields after pasting
$('body').on('paste', '.select2-input', function() {
// append a delimiter and trigger an update
$(this).val(this.value + ',').trigger('input');
});
This assumes that commas are set as delimiters in the plug-in's "tokenSeparators" initialization setting.
For 4.0.1 version:
$('#my-select').data('select2').dataAdapter.$search.val("tag1,tag2,").trigger("input");
This will add two tags: tag1 and tag2 (note trailing ,).
Important: you should add data: [] into select2 init parameters.
Use an input type text, and assign the select2 to it. Like
<input type="text" id="makeTokens" />
and then in javascript
$("#makeTokens").select2({
placeholder: "Paste data",
tags: ["red", "green", "blue"],
tokenSeparators: [",", " "]
});
in the tags, you can assign any values that you want it to display as select options and use the tokenSeperators to seperate the text on commas or spaces etc.
Note: The resultant input value will be comma seperated tokens.
For some reason Donald's solution didn't work for me (maybe newer versions of select2 behaves differently). This is what worked for me:
$('body').on('paste', '.select2-input', function (e) {
var pasteData = (e.originalEvent || e).clipboardData.getData('text/plain') || '';
$(this).val(pasteData + ',');
e.preventDefault();
});
Since at the point the event was triggered the value of .select2-input was an empty string, I extractacted the pasted string from the event object. Apparently the default select2 for copying action was still triggering after this, so I had to add e.preventDefault(); to stop it from running and messing up the input.
just run this jQuery which takes the separatoes from options.tokenSeparators directly, and applies for all select2 instances in the page automatically:
$(document).on('paste', 'span.select2', function (e) {
e.preventDefault();
var select = $(e.target).closest('.select2').prev();
var clipboard = (e.originalEvent || e).clipboardData.getData('text/plain');
var createOption = function (value, selected) {
selected = typeof selected !== 'undefined' ? selected : true;
return $("<option></option>")
.attr("value", value)
.attr("selected", selected)
.text(value)[0]
};
$.each(
clipboard.split(new RegExp(select.data('select2').options.options.tokenSeparators.map(function (a) {
return (a).replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}).join('|'))),
function (key, value) {
if (value && (!select.val() || (select.val() && select.val().indexOf('' + value) == -1))) {
select.append(createOption(value));
}
});
select.trigger('change');
});

Need Help to write a jasmine spec

Hello I am trying to write a jasmine test for a backbone view and one of its function. I want to test the correct behavior of the function in the case a user checks a checkbox in the rendered view then submit.
Here is the tests :
describe("buildCI()", function() {
describe("with a category selection allowed's quidget model", function() {
it("returns a CoacheeIssue model with the selected categories", function() {
selection_allowed = true;
lcc_selection_allowed = false;
var view = new Rails3DeviseRspecCucumber.Views.CategoryPicker({
collection: categoriesCollection,
answers: answers,
category_ids: category_ids,
credentials: credentialsCollection,
user_hash: user_hash,
selection_allowed: selection_allowed,
lcc_selection_allowed: lcc_selection_allowed
});
// render the view so we can manipulate its DOM elements
view.render();
elDebug = $(view.$el);
// Check programmatically a category checkbox
$(elDebug.find('input.category-checkbox#5061c6a48624da6f4100000a')[0]).prop('checked', true);
// call the buildCI() function and check the result
result = view.buildCI();
console.log(result);
expect(result.get('categories')).toContain('category one');
expect(result.get('categories')).not.toContain('category two');
})
})
Unfortunately the test fails with this message : Expected [ ] to contain 'category one'.
I know it is not a coding error, because it is working in live, I would just like to know how to test it.
Here is the function :
buildCI: () ->
# prepare the category_ids and categories (names) attributes
if #selection_allowed
selectedCategories = []
for checkbox in $('input.category-checkbox')
checkboxEl = $(checkbox)
if checkbox.checked
selectedCategories.push(_.find(#collection.models, (model) ->
model.id == checkboxEl.attr('id')
))
category_names = _.map(selectedCategories, (category) -> category.get('name'))
category_ids = _.map(selectedCategories, (category) -> category.get('_id'))
else
category_names = _.map(#collection.models, (category) -> category.get('name'))
category_ids = _.map(#collection.models, (category) -> category.get('_id'))
return new Rails3DeviseRspecCucumber.Models.CoacheeIssue({
is_solved: false, status: 'active', solution_value_estimate: '',
answers: #answers, categories: category_names, category_ids: category_ids
})
Thanks in advance
Is your selector too strict? I notice that it is:
$(elDebug.find('input.category-checkbox#5061c6a48624da6f4100000a')[0]).prop('checked', true);
but perhaps you only want it to be just:
$(elDebug.find('input.category-checkbox')[0]).prop('checked', true);

jquery-ui autocomplete using multiple field values

I have a bunch of fields (first name, last name, etc) and I want to be able to pass the values of any that are filled or partially filled to the autocomplete server side so I can use them in the query.
I did this by updating the autocomplete "source" on any change of focus on the various fields, or on the autocomplete "select" event.
jQuery('#people_new_user input[type="text"]').each(
function(index, element) {
var field = element.name;
jQuery(element)
.focus(setSourceUser)
.autocomplete({
source: "/cf/AutoComplete/People?current="+field,
select: selectUser
});
});
The code above sets up a "focus" event handler called "setSourceUser" and a handler for the autocomplete "select" event called "selectUser".
function setSourceUser(event)
{
var endPart = "";
jQuery('#people_new_user input[type="text"]').each(
function(index, ielement) {
var ifield = ielement.name;
var ival = ielement.value;
if (ival != '')
{
endPart += '&field=' + ifield;
endPart += '&value=' + ival;
}
}).each(
function(index, element) {
var field = element.name;
jQuery(element)
.autocomplete("option", "source",
"/cf/AutoComplete/People?current="+field+endPart);
});
}
The above "setSourceUser" function gets all the values from all the fields (in the first "each" function) and builds a "endPart" for the source, and then sets up the autocomplete "source" option for each field. I'm not going to show the "select" callback because it does other stuff that's not relevant to this issue, and then calls "setSourceUser". The source ends up being something like /cf/AutoComplete/People?current=last_name&field=first_name&value=p&field=last_name&value=tomblin as well as the "term" value that autocomplete itself supplies.
On the server side, my function (written in Mason and Perl in this case) uses the &field=foo&value=bar pairs (skipping the ones where field == current, because autocomplete passes in the more current value in term) in "LIKE" sql statements. Then I return the found results in JSON. (If there are more than 50 results, I don't bother because the list would be too long.)
% $r->content_type('application/json');
<% JSON::to_json( \#suggestions, { utf8 => 1, allow_blessed => 1,
convert_blessed => 1, } ) |n %>
% $m->abort;
<%ARGS>
#field => undef
#value => undef
$current => undef
$term => undef
</%ARGS>
<%INIT>
use RTx::SearchBuilder::Records::Peoples;
$current =~ s/people_//g;
my $people = RTx::SearchBuilder::Records::Peoples->new(Handle => CFHandle());
my $fn = scalar(#field);
for my $i (0...$fn-1)
{
my $f = $field[$i];
next if !defined($f);
my $v = $value[$i];
if ($f ne $current)
{
$people->Limit(
FIELD => $f,
OPERATOR => 'LIKE',
VALUE => '%'.$v.'%',
ENTRYAGGREGATOR => 'AND');
}
}
$people->Limit(
FIELD => $current,
OPERATOR => 'LIKE',
VALUE => '%'.$term.'%',
ENTRYAGGREGATOR => 'AND');
my #suggestions;
# If there are too many results, skip it and make them narrow it down a bit
# more
if ($people->Count < 50)
{
while (my $person = $people->Next)
{
my $suggestion = { label => $person->$current, value => $person };
push #suggestions, $suggestion;
}
}
</%INIT>

Resources