When a SignalR update comes through to our page to update our modal, our item names change and our scripts seem to break.
Brief overview: Our SignalR update gets sent to the website fine, but the data itself has an invalid name.
Once updated, our items refresh with malformed names. Our names shouldn't be updated by SignalR in the first place, and I can't seem to find any references to it in our code.
Closing the modal, our Highcharts and Angular scripts throw console errors.
Server-side code:
public partial class Device
{
if (device != null)
{
if ((Enumerables.DeviceType)device.Type == Enumerables.DeviceType.Store)
SignalrClient.UpdateStore(device.DeviceID);
else // check if need to update a modal on the dashboard
{
foreach (var key in SignalrClient.DevicesDictionary.Keys)
{
var devices = SignalrClient.DevicesDictionary[key];
if (devices != null)
{
if (devices.Contains(device.DeviceID))
SignalrClient.UpdateModal(key, device.DeviceID);
}
}
}
}
}
class SignalrClient
{
public static async Task Start()
{
if (_hubConnection == null || _hubConnection.State == ConnectionState.Disconnected)
{
_hubConnection = new HubConnection("http://stevessiteofamazingboats.net/");
_dashboardHubProxy = _hubConnection.CreateHubProxy("DashboardHub");
_dashboardHubProxy.On("OnRegisterDevice", new Action<string, int>(OnRegisterDevice));
_dashboardHubProxy.On("OnDeregisterDevices", new Action<string>(OnDeregisterDevices));
_dashboardHubProxy.On("OnDeregisterDevice", new Action<string, int>(OnDeregisterDevice));
await _hubConnection.Start();
}
}
public static async void UpdateModal(string connectionId, int deviceId)
{
await Start();
if (_hubConnection.State == ConnectionState.Connected)
await _dashboardHubProxy.Invoke("UpdateModal", new object[] { connectionId, deviceId });
}
}
public class DashboardHub : Hub
{
private static string EventHubConnectionId {get;set;}
private AlarmDBEntities db = Utils.DbContext;
public void UpdateModal(string connectionId, int deviceId)
{
var db = Utils.DbContext;
var device = db.Device.Find(deviceId);
var modal = new Portal.DeviceModalViewModel()
{
DeviceId = deviceId,
SuctionGroups = device.Device1.Where(x => (Enumerables.DeviceType)x.Type == Enumerables.DeviceType.SuctionGroup).Select(x => new DeviceModalViewModel.SGNode()
{
SubChildren = x.Device1.Where(y => (Enumerables.DeviceType)y.Type == Enumerables.DeviceType.Compressor).Select(y => new DeviceModalViewModel.DeviceNode()
{
DeviceId = y.DeviceID,
Name = y.Name,
Amp = db.Property.Where(z => z.Name == "Amps" && z.DeviceID == y.DeviceID).OrderByDescending(z => z.CreatedOn).Select(z => z.Value).FirstOrDefault()
}).OrderBy(y => y.Name).ToList()
}).OrderBy(x => x.Name).ToList(),
};
}
Client-side javascript. The viewModel contains a malformed name:
Viewable on JSFiddle
https://jsfiddle.net/wmqdyv8r/
This is our Angular console error:
angular.min.js:6 Uncaught Error: [ng:areq]
http://errors.angularjs.org/1.5.7/ng/areq?
p0=HeaderController&p1=not%20a%20function%2C%20got%20undefined
at angular.min.js:6
at sb (angular.min.js:22)
at Qa (angular.min.js:23)
at angular.min.js:89
at ag (angular.min.js:72)
at m (angular.min.js:64)
at g (angular.min.js:58)
at g (angular.min.js:58)
at g (angular.min.js:58)
at g (angular.min.js:58)
angular.min.js:312 WARNING: Tried to load angular more than once.
This Highcharts error shows up if we try to open a chart after the SignalR refresh:
store.js:856 Uncaught TypeError: $(...).highcharts is not a function
at Object.success (store.js:856)
at c (<anonymous>:1:132617)
at Object.fireWith [as resolveWith] (<anonymous>:1:133382)
at b (<anonymous>:1:168933)
at XMLHttpRequest.<anonymous> (<anonymous>:1:173769)
Also, after closing the modal, our main page will refresh and now throws this error:
Exception: Sequence contains no elements
Type: System.InvalidOperationException
The main concern is that the update event is breaking something. The naming issue is a lower priority although I'm sure it's related.
Found and fixed the problem!
The malformed name was found to always be trimming the first 5 characters off of the real name, so I fixed that down near the bottom, although I still don't know where this trimming occurs.
The more serious issue, the breaking of scripts was solved as well.
In the UpdateModal method, one of the scripts was looking for a storeID field, along with the deviceID field. After printing a log to the Chrome javascript console, I could see that storeID was always returning 0, even though it was previously initialized before the UpdateModal method.
All I had to do, was follow the damn train add the storeID field seen here under DeviceId:
public void UpdateModal(string connectionId, int deviceId)
{
var db = Utils.DbContext;
var device = db.Device.Find(deviceId);
var modal = new Portal.DeviceModalViewModel()
{
DeviceId = deviceId,
*THIS*-> StoreId = db.Device.Where(x => device.Name.Contains(x.Name.Replace("-Store", "")) && x.ParentID == null).Select(x => x.DeviceID).FirstOrDefault(),
SuctionGroups = device.Device1.Where(x => (Enumerables.DeviceType)x.Type ==
Enumerables.DeviceType.SuctionGroup).Select(x => new
DeviceModalViewModel.SGNode()
{
SubChildren = x.Device1.Where(y => (Enumerables.DeviceType)y.Type ==
Enumerables.DeviceType.Compressor).Select(y => new
DeviceModalViewModel.DeviceNode()
{
DeviceId = y.DeviceID,
*ALSO THIS*-> Name = "12345Comp " + y.Name.Substring(y.Name.Length - 2),
Amp = db.Property.Where(z => z.Name == "Amps" && z.DeviceID == y.DeviceID).OrderByDescending(z => z.CreatedOn).Select(z => z.Value).FirstOrDefault()
}).OrderBy(y => y.Name).ToList()
}).OrderBy(x => x.Name).ToList(),
};
Related
While developing a custom app for my organization, I am trying to request the name and the avatar of the individual accessing the card. I am able to get the name of the individual without any problems, but when requesting the avatar image I get the following console error:
Uncaught (in promise) Error: Invalid JSON response at XMLHttpRequest.d.onload (domo.ts:309:18)
I have looked into the domo.js code, and after making some limited sense of things, I found that it tries to JSON.parse the .png that is returned.
When checking the network dev tools tab I can see the correct image getting returned, but it doesn't get passed to the app.
Here is the function that returns the error:
d.onload = function() {
var e;
if( u(d.status) ) {
!["csv","excel"].includes(r.format) && d.response || i(d.response), "blob" === r.responseType && i(new Blob([d.response], { type:d.getResponseHeader("content-type") }));
var t = d.response;
try{
e = JSON.parse(t)
}
catch(e){
return void c(Error("Invalid JSON response"))
}i(e)
}else c(Error(d.statusText))
}
As far as I can tell, e refers to the Domo environment, although I am not 100% sure of that.
Note: I am turning to stackoverflow because my organization still has open support tickets with Domo that are more than 2 years old with no response, so I have little faith in getting a timely response from Domo regarding this issue.
UPDATE: Here is the full function that is called-
function i(e,t,r,n,a) {
return r = r || {}, new Promise((function(i,c) {
var d = new XMLHttpRequest;
if (n?d.open(e,t,n):d.open(e,t), p(d,t,r), function(e,t) {
t.contentType ?
"multipart" !== t.contentType && e.setRequestHeader("Content-Type", t.contentType)
: e.setRequestHeader("Content-Type", o.DataFormats.JSON)
} (d,r), function(e) {
s && e.setRequestHeader("X-DOMO-Ryuu-Token", s)
} (d), function(e,t) {
void 0 !== t.responseType && (e.responseType = t.responseType)
} (d,r),
d.onload = function() {
var e;
if( u(d.status) ) {
!["csv","excel"].includes(r.format) && d.response || i(d.response), "blob" === r.responseType && i(new Blob([d.response], { type:d.getResponseHeader("content-type") }));
var t = d.response;
try{
e = JSON.parse(t)
}
catch(e){
return void c(Error("Invalid JSON response"))
}i(e)
}else c(Error(d.statusText))
},
d.onerror = function() {
c(Error("Network Error"))
}, a)
if (r.contentType && r.contentType !== o.DataFormats.JSON) d.send(a);
else {
var f = JSON.stringify(a);
d.send(f)
}
else d.send()
}))
Here is the domo.js method that is being called to get the image:
e.get = function(e, t) {
return i(o.RequestMethods.GET, e, t)
},
#Skousini you can get the avatar for a user by providing this URL directly to the src property of the <img> tag (obviously replacing the query params with the relevant information):
<img src="/domo/avatars/v2/USER/846578099?size=300&defaultForeground=fff&defaultBackground=000&defaultText=D" />
This documentation is available on developer.domo.com: https://developer.domo.com/docs/dev-studio-references/user-api#User%20Avatar
If you want to pull down data from endpoints, you don't have to use domo.js. You could use axios or any other HTTP tool. domo.js is trying to make HTTP requests simpler by automatically parsing json (since most requests are json based). There are a few other options for what data format that domo.get can support provided in this documentation: https://developer.domo.com/docs/dev-studio-tools/domo-js#domo.get
The following component's code is from an Angular 6 web application that I am creating. The app displays a table with CRUD functionality. I have an Angular service called GetDBValuesService that is connected to a database and uses DBValues() to retrieve an array of arrays (each inner array contains the values of a given row in the database). My code then collects rows whose 'Number' attribute is equal to 10. These rows are then used by my EventEmitter dataItems, which allows them to be displayed in my web page's CRUD table.
I have created another Angular service called DataService that receives an integer value from another component and sends that value to the shown component (after being subscribed to). I subscribed to this service in the following code and let an instance of gotdata (a public var declared in this component) receive the service's value. However, when I try to use this instance outside of that subscription (to replace the hardcoded 10 described above), this.gotdata is undefined.
How can I modify my code so that I can use the value given by the DataService service in the GetDBValuesService service? Currently, the below code does work due to the hardcoded 10, but does not if I remove that line. Thank you for taking the time to read this.
This is the portion of my CRUD component:
refresh = () => {
this.DataService.DataID$.subscribe((data) => {
this.gotdata = data;
console.log(this.gotdata); //10 (value from console)
});
console.log(this.gotdata); //undefined (value from console)
this.gotdata = 10; //hardcoded value allows further functionality, will be removed when this.gotdata retains its value from the above subscription
if (this.gotdata != null) {
this.GetDBValuesService.DBValues().subscribe((result) => {
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info); //sets rows to be displayed in the web page's table (used by component's HTML file)
});
}}
this.gotdata is undefined because the data is not resolved yet.
refresh = () => {
this.DataService.DataID$.subscribe((data) => {
this.gotdata = data;
console.log(this.gotdata); //10 (value from console)
if (this.gotdata != null) {
this.GetDBValuesService.DBValues().subscribe((result) => {
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info); //sets rows to be displayed in the web page's table (used by component's HTML file)
});
}}
});
Or you can put it inside subscription on complete callback:
this.service.subscribe((data) => {
// code here
},
(error) => console.error(error),
() => {
// do stuff.
});
The problem is that, at the time when you are calling the console.log(...) and the code below, data from the dataID$observable are still on way to you. ( Why do u need to work with observables?)
The best approach for this would to be use the RXJS switchMap operator (what is switchMap?). Because as I see you want to subscribe to the first observable and after that subscribe to another observable. So it can be done this way:
refresh = () => {
this.DataService.DataID$.pipe(switchMap(data: any) => {
if (data) {
this.gotdata = data;
return this.GetDBValuesService.DBValues();
} else {
return of(null); // if 'data' are null, return "empty" observable
}
})).subscribe((result: any) => {
if (!result) {
return; // return if 'result' is null (so the code bellow won't be executed)
}
var a = 0;
for (var i = 0; i < result.length; i++) {
if (result[i].Number == this.gotdata) {
this.info[a] = result[i];
a = a + 1;
}
}
this.dataItems.next(this.info);
});
In my add-in I am making an HTTP request and receiving an output. I want to place that output into a binding and have it expand the binding if necessary because the user won't necessarily know how many rows x columns the output will be. How would I go about doing this? Currently I am binding to a range, but if that range does not match the size of the [[]] that I am providing, then the data is not displayed in the sheet. So, this ends up requiring the user to know the size of the output.
What I'm doing currently using Angular is as follows (the problem with this being that the output isn't always the same size as the Office.BindingType.Matrix that the user selected in the spreadsheet):
I create the binding to where the output should be placed as follows:
inputBindFromPrompt(parameterId: number): Promise<IOfficeResult> {
let bindType: Office.BindingType;
if(this.inputBindings[parameterId].type != 'data.frame' && this.inputBindings[parameterId].type != 'vector') {
bindType = Office.BindingType.Text;
} else {
bindType = Office.BindingType.Matrix;
}
return new Promise((resolve, reject) => {
this.workbook.bindings.addFromPromptAsync(bindType, { id: this.inputBindings[parameterId].name },
(addBindingResult: Office.AsyncResult) => {
if(addBindingResult.status === Office.AsyncResultStatus.Failed) {
reject({
error: 'Unable to bind to workbook. Error: ' + addBindingResult.error.message
});
} else {
this.inputBindings[parameterId].binding = addBindingResult.value;
resolve({
success: 'Created binding ' + addBindingResult.value.type + ' on ' + addBindingResult.value.id
});
}
})
})
}
Then when the user submits via a button, the inputs are passed to a HTTP request service which then receives an output that I process into an array of arrays so that it can go into an Office.BindingType.Matrix:
this.isBusy = true;
this.feedback = 'submitted';
// Grab the values from the form
// Send as a POST and receive an output
// Put the output in the Excel sheet
this.webServicesService.postWebServices(this.service, this.inputParameters)
.subscribe(
(data: any) => {
// Correctly received data
// Access the data by name while looping through output parameters
this.error = false;
this.feedback = 'received data';
let i = 0;
this.outputParameters.forEach(element => {
// temporary name to identify the parameter
let name = element.name;
// Set the data value in the parameter
if(element.type == 'data.frame') {
let parameter = data[name];
this.feedback = parameter;
let excelData = [];
for(var key in parameter) {
if(parameter.hasOwnProperty(key)) {
var val = parameter[key];
excelData.push(val);
}
}
element.value = excelData;
}
else {
element.value = data[name];
}
// Set value in the form
let param = (<FormArray>this.serviceForm.controls['outputParameters']).at(i);
param.patchValue({
value: element.value
});
// Set value in the spreadsheet
this.excelService.outputSetText(i, element.value)
.then((result: IOfficeResult) => {
this.onResult(result);
i++;
});
}, (result: IOfficeResult) => {
this.onResult(result);
});
},
(error) => {
if(error.status == 400 || error.status == 401) {
// Return user to authentication page
this.authService.logout();
this.router.navigate(['/']);
} else {
// Tell user to try again
this.error = true;
}
}
);
The line above that is setting the value to the Office.Matrix.Binding is this.excelService.outputSetText(i, element.value), which calls this method in the Excel Service:
outputSetText(parameterId: number, data: any): Promise<IOfficeResult> {
return new Promise((resolve, reject) => {
if(this.outputBindings[parameterId].binding) {
this.outputBindings[parameterId].binding.setDataAsync(data, function (result: Office.AsyncResult) {
if(result.status == Office.AsyncResultStatus.Failed) {
reject({ error: 'Failed to set value. Error: ' + result.error.message });
} else {
let test: Office.Binding;
resolve({
success: 'successfully set value'
});
}
})
} else {
reject({
error: 'binding has not been created. bindFromPrompt must be called'
});
}
})
}
It's essentially using addFromPromptAsync() to set an output spot for the HTTP request. Then the user submits which sends the request, receives the data back and processes it into an array of arrays [[]] so that it can be the correct data format for Office.BindingType.Matrix. However, unless this is the same number of rows and columns as the binding originally selected, it won't display in the sheet. So, is there a binding type that will dynamically grow based on the data I give it? Or would I just need to release the current binding and make a new binding according to the size of the HTTP response data?
So long as you're using the "shared" (Office 2013) APIs, you will have this issue.
However, in the host-specific (2016+) APIs, you can easily solve the problem by resizing the range to suit your needs. Or more precisely, getting the binding, then asking for its range, then getting just the first (top-left) cell, and then resizing it:
await Excel.run(async (context) => {
let values = [
["", "Price"],
["Apple", 0.99],
["Orange", 1.59],
];
let firstCell = context.workbook.bindings.getItem("TestBinding").getRange().getCell(0, 0);
let fullRange = firstCell.getResizedRange(
values.length - 1, values[0].length - 1);
fullRange.values = values;
await context.sync();
});
You can try this snippet live in literally five clicks in the new Script Lab (https://aka.ms/getscriptlab). Simply install the Script Lab add-in (free), then choose "Import" in the navigation menu, and use the following GIST URL: https://gist.github.com/Zlatkovsky/5a2fc743bc9c8556d3eb3234e287d7f3. See more info about importing snippets to Script Lab.
Requirements when clicking the Qualify button in the Lead entity form:
Do not create an Opportunity
Retain original CRM qualify-lead JavaScript
Detect duplicates and show duplicate detection form for leads
Redirect to contact, either merged or created version, when done
The easiest approach is to create a plugin running on Pre-Validation for message "QualifyLead". In this plugin you simply have to set CreateOpportunity input property to false. So it would look like:
public void Execute(IServiceProvider serviceProvider)
{
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
context.InputParameters["CreateOpportunity"] = false;
}
Or you can go with more fancy way:
public void Execute(IServiceProvider serviceProvider)
{
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
var qualifyRequest = new QualifyLeadRequest();
qualifyRequest.Parameters = context.InputParameters;
qualifyRequest.CreateOpportunity = false;
}
Remember that it should be Pre-Validation to work correctly. Doing it like that allows you to remain with existing "Qualify" button, without any JavaScript modifications.
So Pawel Gradecki already posted how to prevent CRM from creating an Opportunity when a Lead is qualified. The tricky part is to make the UI/client refresh or redirect to the contact, as CRM does nothing if no Opportunity is created.
Before we begin, Pawel pointed out that
some code is not supported, so be careful during upgrades
I don't have experience with any other versions than CRM 2015, but he writes that there are better ways to do this in CRM 2016, so upgrade if you can. This is a fix that's easy to implement now and easy to remove after you've upgraded.
Add a JavaScript-resource and register it in the Lead form's OnSave event. The code below is in TypeScript. TypeScript-output (js-version) is at the end of this answer.
function OnSave(executionContext: ExecutionContext | undefined) {
let eventArgs = executionContext && executionContext.getEventArgs()
if (!eventArgs || eventArgs.isDefaultPrevented() || eventArgs.getSaveMode() !== Xrm.SaveMode.qualify)
return
// Override the callback that's executed when the duplicate detection form is closed after selecting which contact to merge with.
// This callback is not executed if the form is cancelled.
let originalCallback = Mscrm.LeadCommandActions.performActionAfterHandleLeadDuplication
Mscrm.LeadCommandActions.performActionAfterHandleLeadDuplication = (returnValue) => {
originalCallback(returnValue)
RedirectToContact()
}
// Because Opportunities isn't created, and CRM only redirects if an opportunity is created upon lead qualification,
// we have to write custom code to redirect to the contact instead
RedirectToContact()
}
// CRM doesn't tell us when the contact is created, since its qualifyLead callback does nothing unless it finds an opportunity to redirect to.
// This function tries to redirect whenever the contact is created
function RedirectToContact(retryCount = 0) {
if (retryCount === 10)
return Xrm.Utility.alertDialog("Could not redirect you to the contact. Perhaps something went wrong while CRM tried to create it. Please try again or contact the nerds in the IT department.")
setTimeout(() => {
if ($("iframe[src*=dup_warning]", parent.document).length)
return // Return if the duplicate detection form is visible. This function is called again when it's closed
let leadId = Xrm.Page.data.entity.getId()
$.getJSON(Xrm.Page.context.getClientUrl() + `/XRMServices/2011/OrganizationData.svc/LeadSet(guid'${leadId}')?$select=ParentContactId`)
.then(r => {
if (!r.d.ParentContactId.Id)
return RedirectToContact(retryCount + 1)
Xrm.Utility.openEntityForm("contact", r.d.ParentContactId.Id)
})
.fail((_, __, err) => Xrm.Utility.alertDialog(`Something went wrong. Please try again or contact the IT-department.\n\nGuru meditation:\n${err}`))
}, 1000)
}
TypeScript definitions:
declare var Mscrm: Mscrm
interface Mscrm {
LeadCommandActions: LeadCommandActions
}
interface LeadCommandActions {
performActionAfterHandleLeadDuplication: { (returnValue: any): void }
}
declare var Xrm: Xrm
interface Xrm {
Page: Page
SaveMode: typeof SaveModeEnum
Utility: Utility
}
interface Utility {
alertDialog(message: string): void
openEntityForm(name: string, id?: string): Object
}
interface ExecutionContext {
getEventArgs(): SaveEventArgs
}
interface SaveEventArgs {
getSaveMode(): SaveModeEnum
isDefaultPrevented(): boolean
}
interface Page {
context: Context
data: Data
}
interface Context {
getClientUrl(): string
}
interface Data {
entity: Entity
}
interface Entity {
getId(): string
}
declare enum SaveModeEnum {
qualify
}
TypeScript-output:
function OnSave(executionContext) {
var eventArgs = executionContext && executionContext.getEventArgs();
if (!eventArgs || eventArgs.isDefaultPrevented() || eventArgs.getSaveMode() !== Xrm.SaveMode.qualify)
return;
var originalCallback = Mscrm.LeadCommandActions.performActionAfterHandleLeadDuplication;
Mscrm.LeadCommandActions.performActionAfterHandleLeadDuplication = function (returnValue) {
originalCallback(returnValue);
RedirectToContact();
};
RedirectToContact();
}
function RedirectToContact(retryCount) {
if (retryCount === void 0) { retryCount = 0; }
if (retryCount === 10)
return Xrm.Utility.alertDialog("Could not redirect you to the contact. Perhaps something went wrong while CRM tried to create it. Please try again or contact the nerds in the IT department.");
setTimeout(function () {
if ($("iframe[src*=dup_warning]", parent.document).length)
return;
var leadId = Xrm.Page.data.entity.getId();
$.getJSON(Xrm.Page.context.getClientUrl() + ("/XRMServices/2011/OrganizationData.svc/LeadSet(guid'" + leadId + "')?$select=ParentContactId"))
.then(function (r) {
if (!r.d.ParentContactId.Id)
return RedirectToContact(retryCount + 1);
Xrm.Utility.openEntityForm("contact", r.d.ParentContactId.Id);
})
.fail(function (_, __, err) { return Xrm.Utility.alertDialog("Something went wrong. Please try again or contact the IT-department.\n\nGuru meditation:\n" + err); });
}, 1000);
}
There is a fully functional and supported solution posted over at our Thrives blog: https://www.thrives.be/dynamics-crm/functional/lead-qualification-well-skip-that-opportunity.
Basically we combine the plugin modification as mentioned by Pawel with a Client Side redirect (using only supported JavaScript) afterwards:
function RefreshOnQualify(eventContext) {
if (eventContext != null && eventContext.getEventArgs() != null) {
if (eventContext.getEventArgs().getSaveMode() == 16) {
setTimeout(function () {
Xrm.Page.data.refresh(false).then(function () {
var contactId = Xrm.Page.getAttribute("parentcontactid").getValue();
if (contactId != null && contactId.length > 0) {
Xrm.Utility.openEntityForm(contactId[0].entityType, contactId[0].id)
}
}, function (error) { console.log(error) });;
}, 1500);
}
}
}
Following scenario.
I wrote a angular2 application with material2.
In my SideNav is a search input field. When a user types in it, he is redirected (via routing) to the search component, while the searched word is handed over as a routing parameter.
The search component shows all pages of the application, which contain the searched word (index in the background). Once the user clicks on the entry, he's redirected to this page, and the searched word is appended as a query parameter. I'm now trying to highlight all appearances of the searchword on the page, the user gets redirected to. At the moment i'm doing this:
subscription: ISubscription;
searchTerm: string;
constructor(private router: Router, private elementRef: ElementRef) {}
ngOnInit(): void {
this.subscription = this.router.routerState.queryParams.subscribe(queryParams => {
let searchTerm = queryParams['searchTerm'];
if (searchTerm) {
this.searchTerm = searchTerm;
} else {
this.searchTerm = null;
}
});
}
ngAfterContentInit(): void {
if (this.searchTerm && isStaticDoc) {
let regExp = new RegExp(`(${this.searchTerm})`, 'i');
this.highlightWords(this.elementRef.nativeElement, regExp);
}
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
highlightWords(node, regExp: RegExp) {
if (!node || ! regExp) {
return;
}
if (node.nodeType === 3) {
let regs = regExp.exec(node.nodeValue);
if (regs) {
let match = document.createElement('span');
match.appendChild(document.createTextNode(regs[0]));
match.classList.add('search-hl');
let after = node.splitText(regs.index);
after.nodeValue = after.nodeValue.substring(regs[0].length);
node.parentNode.insertBefore(match, after);
}
} else if (node.hasChildNodes()) {
for (let i = 0; i < node.childNodes.length; i++) {
this.highlightWords(node.childNodes[i], regExp);
}
}
}
Now the issue is, that i get an error RangeError: Maximum call stack size exceeded, which might be a hint, that the recursion level is way to deep.
I've already tried to use 3rd party libraries, bot non of them is really made to be used from angular2 and on top, the written code isn't that difficult... but its not working.
Any ideas how to stage beneath the maximum call stack size following the same or an similar approach?
tl;dr trying to highlight all appearances of searchTerm(which is passed over as a queryParam) on the page -> my approach (see code) is not
working due to max call stack size.
Edit: Using rc4 atm, upgrading soon, but this shouldn't be an issue (i guess)
Thanks to user3791775 I've come up with an solution.
highlightWords(html: string, searchTerm: string): string {
let regExp = new RegExp(`(${searchTerm})`, 'i');
let results = regExp.exec(html);
if (results) {
let before = html.substr(0, results.index);
let after = html.substr(results.index + searchTerm.length);
let indexOpenTag = before.lastIndexOf('<');
let indexCloseTag = before.lastIndexOf('>');
let indexOpenTagAfter = after.indexOf('<');
let indexCloseTagAfter = after.indexOf('>');
if (indexOpenTag <= indexCloseTag && indexOpenTagAfter <= indexCloseTagAfter) {
return `${before}<span class="search-hl">${results[0]}</span>${this.highlightWords(after, searchTerm)}`;
} else {
return `${before}${results[0]}${this.highlightWords(after, searchTerm)}`;
}
} else {
return html;
}
}
This can be used the following way
let ref = document.getElementById('my-highlicht-content');
ref.innerHtml = this.highlightWords(ref.innerHtml, this.searchTerm)
Thanks for helping!
Edit:
Had another edgecase, which made it necessary to inspect the part after the keyword as well. Updated my example.