TagHelper child tag unescaped attribute value - javascript

No matter how I try to skin it, I can't get the output from my TagHelper for the Google Analytics onclick attribute to not be escaped. Specifically the single quotes.
TagBuilder listTag = new TagBuilder("ul");
listTag.AddCssClass(ListClass);
foreach (Category category in Links)
{
bool isSelectedCategory = category.PathValue == ViewContext.RouteData.Values["controller"].ToString()
|| (category.PathValue == "home"
&& ViewContext.RouteData.Values["controller"].ToString().ToLowerInvariant() == "home"
&& ViewContext.RouteData.Values["action"].ToString().ToLowerInvariant() == "index");
TagBuilder listItemTag = new TagBuilder("li");
listItemTag.AddCssClass(ListItemClass);
if (isSelectedCategory) { listItemTag.AddCssClass(ListItemActiveClass); }
TagBuilder linkTag = new TagBuilder("a");
linkTag.InnerHtml.Append(category.Name);
linkTag.AddCssClass(LinkClass);
if (isSelectedCategory) { linkTag.AddCssClass(LinkActiveClass); }
linkTag.Attributes["href"] = urlHelper.RouteUrl("default", new
{
controller = category.PathValue,
action = "index",
topicPathString = category.DefaultTopic?.PathValue,
articlePathString = category.DefaultTopic?.DefaultArticle?.PathValue
});
if (EnableGoogleAnalytics)
{
string gaOnClick = $"ga('send', 'event', 'Navbar', 'Links', '{category.Name}');";
linkTag.MergeAttribute("onclick", gaOnClick);
}
listItemTag.InnerHtml.AppendHtml(linkTag);
listTag.InnerHtml.AppendHtml(listItemTag);
}
output.Content.SetHtmlContent(listTag);
base.Process(context, output);
Outputs onclick="ga'send', 'event', 'Navbar', 'Links', 'Guides');".
What I need is gaOnClick="ga('send', 'event', 'Navbar', 'Links', 'Guides');".
Is this possible with nested tag elements, or do I need to approach it in a different way?

Having spent most of the morning trying to find a solution, it's only fitting that I stumble across one shortly after posting!
Based on Creating html helpers without encoding, but with an added utility class based on Convert IHtmlContent/TagBuilder to string in C#, has solved the problem.
HtmlString decodedLinkTag = new HtmlString(
System.Net.WebUtility.HtmlDecode(
Utility.TagBuilderToString(linkTag)));
listItemTag.InnerHtml.AppendHtml(decodedLinkTag);

Related

Elm not allowing html `script` node in virtual DOM

I try to integrate to my ELM page a "login with" widget (the one from Telegram https://core.telegram.org/widgets/login)
I try to build the appropriate node, which is of type <script>, but when the page is rendered, the type is replaced by <p>. I guess this is a security feature. But how can I build this node ?
What I want:
<script
async
src="https://telegram.org/js/telegram-widget.js?21"
data-telegram-login="botname"
data-onauth="onTelegramAuth(user)"
></script>
What I do:
telegram : Html Model
telegram =
node "script"
[ attribute "async" ""
, attribute "src" "https://telegrami.org/js/telegram-widget.js?21"
, attribute "data-telegram-login" "botname"
, attribute "data-onauth" "onTelegramAuth(user)"
]
[]
What I get:
<p
async=""
src="https://telegrami.org/js/telegram-widget.js?21"
data-telegram-login="botname"
data-onauth="onTelegramAuth(user)"
></p>
Thank for your help :)
This is intentional. Elm doesn't allow script tags for security reasons. If you want that kind of functionality, wrap it around in a web component and import the web component in Elm. You can listen for Custom Events on Elm side in order to pass data from the web component to Elm and you can set attributes to the web component in order to pass data from Elm to the web component.
Thanks to #pdamoc,
I've managed to make it work like this:
Web component: telegram-button.js
export default class TelegramButton extends HTMLElement {
constructor() {
const self = super();
self.onauth = (user) => {
this.dispatchEvent(new CustomEvent('on-telegram-auth', {detail: user}))
}
return self;
}
connectedCallback() {
const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-widget.js?21';
script.async = true;
const attributes = {
'data-telegram-login': this.getAttribute('data-telegram-login'),
'data-size': this.getAttribute('data-size'),
'data-radius': this.getAttribute('data-radius'),
'data-request-access': this.getAttribute('data-request-access'),
'data-onauth': 'onTelegramAuth(user)',
};
for (const [k, v] of Object.entries(attributes)) {
v !== undefined && script.setAttribute(k, `${v}`);
}
this.appendChild(script);
}
}
const onTelegramAuth = (user) => {
const button = document.querySelector("telegram-button")
button.onauth(user)
}
if (!window.customElements.get('telegram-button')) {
window.TelegramButton = TelegramButton
window.customElements.define('telegram-button', TelegramButton)
window.onTelegramAuth = onTelegramAuth
}
Import it inside your index.js
Then in Elm
button : Html Msg
button =
Html.node "telegram-button"
[ attribute "data-telegram-login" "MyBot"
, attribute "data-size" "large"
, attribute "data-radius" "6"
, attribute "data-request-access" "write"
, onTelegramAuthChange OnTelegramAuth
] []
onTelegramAuthChange : Msg -> Attribute Msg
onTelegramAuthChange toMsg =
telegramDataDecoder
|> Decode.map toMsg
|> Html.Events.on "on-telegram-auth"
type alias TelegramAuth =
{ id : Int }
telegramDataDecoder : Decode.Decoder TelegramAuth
telegramDataDecoder =
Decode.map TelegramAuth
(Decode.at ["detail", "id"] Decode.int)

How to loop through HTML elements and populate a Json-object?

I'm looping through all the html tags in an html-file, checking if those tags match conditions, and trying to compose a JSON-object of a following schema:
[
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' },
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' },
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' }
]
But I'd like to create the new entry only for elements, classed "header", all the other elements have to be added to earlier created entry. How do I achieve that?
Current code:
$('*').each((index, element) => {
if ( $(element).hasClass( "header" ) ) {
jsonObject.push({
title: $(element).text()
});
};
if( $(element).hasClass( "date" )) {
jsonObject.push({
date: $(element).text()
});
}
//links.push($(element))
});
console.log(jsonObject)
Result is:
{
title: 'TestA'
},
{ date: '10.10.10' },
{
title: 'TestB'
},
{ date: '10.10.11' }
I'd like it to be at this stage something like:
{
title: 'TestA'
,
date: '10.10.10' },
{
title: 'TestB'
,
date: '10.10.11' }
UPD:
Here's the example of HTML file:
<h1 class="header">H1_Header</h1>
<h2 class="date">Date</h2>
<p>A.</p>
<p>B.</p>
<p>С.</p>
<p>D.</p>
<a class="source">http://</a>
<h1 class="header">H1_Header2</h1>
<h2 class="date">Date2</h2>
<p>A2.</p>
<p>B2.</p>
<p>С2.</p>
<p>D2.</p>
<a class="source">http://2</a>
Thank you for your time!
Based on your example Html, it appears everything you are trying to collect is in a linear order, so you get a title, date, body and link then a new header with the associated items you want to collect, since this appears to not have the complication of having things being ordered in a non-linear fasion, you could do something like the following:
let jsonObject = null;
let newObject = false;
let appendParagraph = false;
let jObjects = [];
$('*').each((index, element) => {
if ($(element).hasClass("header")) {
//If newObject is true, push object into array
if(newObject)
jObjects.push(jsonObject);
//Reset the json object variable to an empty object
jsonObject = {};
//Reset the paragraph append boolean
appendParagraph = false;
//Set the header property
jsonObject.header = $(element).text();
//Set the boolean so on the next encounter of header tag the jsobObject is pushed into the array
newObject = true;
};
if( $(element).hasClass( "date" )) {
jsonObject.date = $(element).text();
}
if( $(element).prop("tagName") === "P") {
//If you are storing paragraph as one string value
//Otherwise switch the body var to an array and push instead of append
if(!appendParagraph){ //Use boolean to know if this is the first p element of object
jsonObject.body = $(element).text();
appendParagraph = true; //Set boolean to true to append on next p and subsequent p elements
} else {
jsonObject.body += (", " + $(element).text()); //append to the body
}
}
//Add the href property
if( $(element).hasClass("source")) {
//edit to do what you wanted here, based on your comment:
jsonObject.link = $(element).next().html();
//jsonObject.href= $(element).attr('href');
}
});
//Push final object into array
jObjects.push(jsonObject);
console.log(jObjects);
Here is a jsfiddle for this: https://jsfiddle.net/Lyojx85e/
I can't get the text of the anchor tags on the fiddle (I believe because nested anchor tags are not valid and will be parsed as seperate anchor tags by the browser), but the code provided should work in a real world example. If .text() doesn't work you can switch it to .html() on the link, I was confused on what you are trying to get on this one, so I updated the answer to get the href attribute of the link as it appears that is what you want. The thing is that the anchor with the class doesn't have an href attribute, so I'll leave it to you to fix that part for yourself, but this answer should give you what you need.
$('*').each((index, element) => {
var obj = {};
if ( $(element).hasClass( "header" ) ) {
obj.title = $(element).text();
};
if( $(element).hasClass( "date" )) {
obj.date = $(element).text()
}
jsonObject.push(obj);
});
I don't know about jQuery, but with JavaScript you can do with something like this.
const arr = [];
document.querySelectorAll("li").forEach((elem) => {
const obj = {};
const title = elem.querySelector("h2");
const date = elem.querySelector("date");
if (title) obj["title"] = title.textContent;
if (date) obj["date"] = date.textContent;
arr.push(obj);
});
console.log(arr);
<ul>
<li>
<h2>A</h2>
<date>1</date>
</li>
<li>
<h2>B</h2>
</li>
<li>
<date>3</date>
</li>
</ul>
Always use map for things like this. This should look something like:
let objects = $('.header').get().map(el => {
return {
date: $(el).attr('date'),
title: $(el).attr('title'),
}
})

angular 7 add html with binding after view init

I have an issue here,
i want to add to my html this code
<dx-report-viewer [reportUrl]="reportUrl" height="800px">
<dxrv-request-options [invokeAction]="invokeAction" [host]="hostUrl">
</dxrv-request-options>
</dx-report-viewer>
but only after getting params from
self.queryParams = self.route.queryParams.subscribe(params => {
if (params['id']) {
self.ReportId = params['id']; // (+) converts string 'id' to a number
console.log(self.ReportId);
self.reportUrl += "ReportId=" + self.ReportId;
}
Because otherwise the url will be wrong ...
but i can't get it work is it because dx-report is a directive from an external library ?
If someone can help would be great !!
Create a property and default it to false:
paramsAdded: boolean = false;
Wrap the report element in a div using *ngIf:
<div *ngIf="paramsAdded">
<dx-report-viewer [reportUrl]="reportUrl" height="800px">
<dxrv-request-options [invokeAction]="invokeAction" [host]="hostUrl">
</dxrv-request-options>
</dx-report-viewer>
</div>
When the parameters are available set the property to true to render the report element(s):
self.queryParams = self.route.queryParams.subscribe(params => {
if (params['id']) {
self.ReportId = params['id']; // (+) converts string 'id' to a number
console.log(self.ReportId);
self.reportUrl += "ReportId=" + self.ReportId;
self.paramsAdded = true;
}

Cannot get correct syntax for #Html.ListBoxFor()

I've a series of #Html components which are built dynamically including ListBoxFor(). With the others I've given them an ID which I then use to populate a model value called inputvalues, which holds the values of each component whenever it changes. This works well but I had to change the original DropDownListFor() for ListBoxFor() but although the new syntax works, I cannot assign it an ID value as I did before without getting a syntax error. The code looks like this..
#if (Model != null)
{
#Styles.Render(BundleConfig.Styles_MultiSelect)
IEnumerable<SelectListItem> filetypes = from filetype in Model.ListOptions
select new SelectListItem
{
Value = filetype.ID.ToString(),
Text = filetype.Name,
Selected = Model.SelectedListOptionID == null ? false : Model.SelectedListOptionID > 0
};
<div class="editor-section">
<div class="label">
#Html.DisplayEditLabel(Model.Label, Model.Required.Value)
</div>
<div class="field large-text-field">
#*Original drop down replaced by ListBoxFor() but with ID
#Html.DropDownListFor(m => m.SelectedListOptionID, new SelectList(Model.ListOptions, "ID", "Name", Model.SelectedListOptionID).OrderBy(l => l.Value), new Dictionary<string, object>{
{"id", "personField_" + Model.ID}})*#
#Html.ListBoxFor(m => m.ListOptions, filetypes, new { #class = "multiselectFileTypes" })
</div>
</div>
}
#Scripts.Render(BundleConfig.Scripts_MultiSelect)
<script>
$("#personField_" + "#Model.ID").change(function () {
cnt++;
var uploadValue = JSON.stringify({
"id": "#Model.ID",
"order": cnt,
"required": "#Model.Required",
"libraryUploadConfigType": 3,
"customFieldTypeID": 5,
"selectedListOptionID": $(this).val()
});
inputValues = inputValues + uploadValue;
});
$(".multiselectFileTypes").multiselect({
noneSelectedText: 'All Options',
minWidth: 230,
selectedList: 6
});
</script>
Although the syntax for the original DropDownlistFor() worked and updated inputvalues the component didn't work. Having changed it to ListBoxFor() the component works but I can't seem to assign the ID 'personField_' without getting an error.
Any help would be appreciated.
I can't see that you try to assign ID in your ListBoxFor helper.
It should be like this:
#Html.ListBoxFor(m => m.SelectedListOptionIDs, filetypes, new { #class = "multiselectFileTypes" })
And SelectedListOptionIDs field of your model should be IList or IEnumerable, or Array of your ID type (probably IList<int>). Then it will work fine on View and bind correct on form POST.
See reply from Stephen Meucke above.

Angular Chosen default not working with object

I am using https://github.com/localytics/angular-chosen to allow for select tags with search capability for many options.
The problem I'm having is with preselecting an option on an already saved vendor object. When creating a new one there is now issue, but if we're viewing an existing vendor, I want to show the vendor's name in the select box, rather than the placeholder.
<select chosen
ng-model="myVendor"
ng-options="vendor['public-id'] as vendor.name for vendor in vendors"
data-placeholder="Nah">
</select>
And in my controller, I'm setting the model by hand $scope.myVendor = "Some value"
The problem is that I'm populating the options with an object, instead of a key/value. I found an example of it working with a key/value, but haven't had success adapting this to objects as options.
I've even tried setting myVendor to the matching object that I want selected, with no luck.
Plunker of issue
I updated the plunker and change my previous changes on the plugin. this was not the issue. I don't understand how it was giving me errors there.
The solution is to track with an object and two functions the id and the name:
// Controller
$scope.vendors = [
{
"public-id": "1234",
"name": "stugg"
},
{
"public-id": "4321",
"name": "pugg"
}
];
$scope.myVendor = {name: "pugg", id:""};
$scope.updateMyVendorName = function () {
var found = false,
i = 0;
while (!found && i < $scope.vendors.length) {
found = $scope.vendors[i]['public-id'] === $scope.myVendor.id;
if (found) {
$scope.myVendor.name = $scope.vendors[i].name;
}
i++;
}
}
findVendorByName();
function findVendorByName () {
var found = false,
i = 0;
while (!found && i < $scope.vendors.length) {
found = $scope.vendors[i]['name'] === $scope.myVendor.name;
if (found) {
$scope.myVendor.id = $scope.vendors[i]['public-id'];
}
i++;
}
}
// template
<select chosen class="form-control span6" ng-options="vendor['public-id'] as vendor.name for vendor in vendors" ng-model="myVendor.id" ng-change="updateMyVendorName()">
{{myVendor.name}}

Categories