Elm not allowing html `script` node in virtual DOM - javascript

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)

Related

Can't access/update class object properties after creation - javascript

The problem is I get "Uncaught ReferenceError: holder is not defined" when trying to access object properties after object creation.
I'm doing the To Do List project for the the Odin Project.
JSFIDDLE: https://jsfiddle.net/appredator/8tcg7yo9/312/
Essentially, I want to click the add button, gather the values from the input fields, and save the values to their correct properties in a corresponding object and then display them as well.
//HTML
<div class="grid-container">
<h2 class="projectName">
Project
</h2>
<h2 class="task">
Task
</h2>
<h2 class="dueDate">
Due Date
</h2>
<h2 class="status">
Status
</h2>
</div>
<!-- End Header Div -->
<!-- Begin To Do List Grid Rows -->
<div class="projects">
</div>
<!-- Button at bottom of page -->
<button id="addProject">
Add To Do
</button>
//JS
class Project {
constructor(name) {
this.name = name;
}
}
class ToDo {
constructor(projectName, task, dueDate, status) {
this.projectName = projectName;
this.task = task;
this.dueDate = dueDate;
this.status = status;
this.list =
[{
task: "",
status: "",
dueDate: "",
}];
}
}
// Add Project Button Logic
const addButton = document.querySelector("#addProject");
addButton.addEventListener('click', addProject);
function addProject(){
let counter = 0;
//Select bottom div for updates etc
var content = document.querySelector(".projects");
//Save projectNameField contents when enter is pressed on the ProjectName Field
const projectNameField = document.createElement("input");
projectNameField.classList.add('projectNameCell');
content.appendChild(projectNameField);
projectNameField.addEventListener('keydown', function onEvent(e) {
if (e.key === 'Enter' || e.key === 'Tab') {
//Get and save project name
/* var project = projectNameField.value; */
//Instantiate Project constructor with DOM value
let project = new Project(projectNameField.value);
console.log(e.key)
projectNameField.remove();
//Use constructor to display project name in text
const projectNameDisplay = document.createElement("h3");
projectNameDisplay.classList.add('projectNameCell');
projectNameDisplay.innerHTML = project.name;
content.appendChild(projectNameDisplay);
}
});
//Save titleField contents when enter is pressed on the ProjectName Field
const taskField = document.createElement("input");
taskField.classList.add('taskCell');
content.appendChild(taskField);
taskField.addEventListener('keydown', function onEvent(e) {
if (e.key === 'Enter' || e.key === 'Tab') {
//Get and save title
//Instantiate toDo constructor with DOM value
let holder = new ToDo(taskField.value);
holder.projectName = document.querySelector(".projectNameCell").innerHTML;
holder.task = taskField.value;
console.log(holder.task)
//Use constructor to display project name in text
const taskDisplay = document.createElement("h3");
taskDisplay.classList.add('titleCell');
taskDisplay.innerHTML = holder.task;
taskField.remove();
content.appendChild(taskDisplay);
}
});
const dueDateField = document.createElement("input");
dueDateField.classList.add('dueDateCell');
content.appendChild(dueDateField);
dueDateField.addEventListener('keydown', function onEvent(e) {
if (e.key === 'Enter' || e.key === 'Tab') {
//Instantiate toDo constructor with DOM value
holder.dueDate = dueDateField.value;
/* console.log(task.list.dueDate) */
dueDateField.remove();
//Use constructor to display project name in text
const dueDateDisplay = document.createElement("h3");
dueDateDisplay.classList.add('dueDateCell');
dueDateDisplay.innerHTML = holder.dueDate;
content.appendChild(dueDateDisplay);
}
});
const statusField = document.createElement("input");
statusField.classList.add('statusCell');
content.appendChild(statusField);
statusField.addEventListener('keydown', function onEvent(e) {
if (e.key === 'Enter' || e.key === 'Tab') {
//Instantiate toDo constructor with DOM value
task.status = statusField.value;
console.log(task.dueDate)
statusField.remove();
//Use constructor to display project name in text
const statusDisplay = document.createElement("h3");
statusDisplay.classList.add('statusCell');
statusDisplay.innerHTML = task.status;
content.appendChild(statusDisplay);
}
});
counter++;
}
**I have tried adding get / set methods to the ToDo class with no luck. I also tried factory functions...
This is a scoping problem right? The properties are local scoped to the holder object?... I cut my code down on this post, but the full code is at jsfiddle. The reference error happens after you hit tab or enter on the 3rd input field.... Please advise. I'm kind of puzzled why objects would be so hard to access and update after creation lol. I created the object so I could use it. I know it's something trivial about scope and when to use constructors/classes/factories, but I cant figure it out.**

How to create an onclick function that retrieves the object ID on the HTML and attach the ID onto the API to be fetched and used?

i'm a beginner developer in javascript trying to make a website to impress recruiters. I've succesfully retrieve information from my API and used it on my website which took me ages to figure out. Now the next problem is how do i make an onclick event that gets the ID i hardcoded onto the specific movie poster on my HTML, to be used in javascript to change the API address and get the data for that specific movie.
Notes: i have a modal onclick to host all the movie data on that modal.
My Javascript and HTML is below.
HTML
<div class="poster" onclick="toggleModal(); getimdbID()" id="tt4154796">
<div id="imdbID">tt4154796</div>
<img
src="https://m.media-amazon.com/images/M/MV5BMTc5MDE2ODcwNV5BMl5BanBnXkFtZTgwMzI2NzQ2NzM#._V1_SX300.jpg"
alt=""
class="poster__img"
/>
<p class="movie__title">Avengers: Endgame</p>
<p class="year">2019</p>
</div>
Javascript
async function getMovie() {
const movies = await fetch(`https://www.omdbapi.com/?i=tt4154796&apikey=4148fa0f`);
const movieData = await movies.json();
const { Title, Year, Runtime, Genre, Director, Actors, Plot, Awards, Poster, Metascore, imdbRating, imdbVotes, BoxOffice, } = movieData
document.getElementById('title').textContent = Title;
document.getElementById('year').textContent = Year;
document.getElementById('genre').textContent = Genre;
document.getElementById('runtime').textContent = Runtime;
document.getElementById('plot').textContent = Plot;
document.getElementById('director').textContent = Director;
document.getElementById('actors').textContent = Actors;
document.getElementById('awards').textContent = Awards;
document.getElementById('metascore').textContent = Metascore;
document.getElementById('boxoffice').textContent = BoxOffice;
document.getElementById('imdbRating').textContent = imdbRating;
document.getElementById('imdbVotes').textContent = imdbVotes;
document.getElementById('poster').src = Poster;
}
getMovie()
// ToggleModal
let isModalOpen = false;
function toggleModal() {
if(isModalOpen) {
isModalOpen = false;
return document.body.classList.remove("modal--open");
}
isModalOpen = true;
document.body.classList += " modal--open";
}
I've tried to create a function onclick to get the value of the ID onclick but that didn't work.
function getimdbID() {
let x = document.querySelector("#imdbID").value
console.log(x)
}
Im guessing you want your function to return the "tt4154796" thats contained within the div. If this is the case you'll want to use document.querySelector("#imdbID").innerText.
Since using .value is typically used to retrieve or change values entered into input fields.

getting more than one items from local storage using javscript

I am currently working on a project and struggling to implement Local Storages on my work.
About My Project:
I am trying to do a basic FILM LIST Project.
I have 3 inputs. I saved them but I am not able to get all items from storage when the "DOMContentLoaded" listener run.
I created a LocalStorage class and imported it to my app.js
I created a FILM class and imported it to my app.js
I created a UI class and imported it to my app.js
I am going to post my important functions and classes for you to check,
Film.js
class Film {
constructor(title, director, url) {
this.title = title;
this.director = director;
this.url = url;
}
}
export default Film;
UI.js
class UI {
constructor() {
}
static addFilmToUI(film) {
//console.log(film.title, film.director, film.url)
let html = ` <tr>
<td><img src="${film.url}" class="img-fluid img-thumbnail"></td>
<td>${film.title}</td>
<td>${film.director}</td>
<td>Delete Film</td>
</tr>`
document.getElementById("films").innerHTML += html;
} ...
LocalStorage.js
class LStorage {
constructor() {
}
static getAllItemsFromLocalStorage() {
let films;
if (localStorage.getItem("films") === null) {
films = [];
} else {
films = JSON.parse(localStorage.getItem("films"));
console.log(films)
}
return films;
}
static addItemsToLocalStorage(film) {
let films = this.getAllItemsFromLocalStorage();
films.push(film);
localStorage.setItem("films",JSON.stringify(films));
}}
App.js
const form = document.querySelector("form");
form.addEventListener("submit", createFilm);
document.addEventListener("DOMContentLoaded",function(){
let films = LS.getAllItemsFromLocalStorage();
Array.prototype.forEach.call(films,(element)=>{
UI.addFilmToUI(element)
})
})
function createFilm(e) {
e.preventDefault()
const title = document.getElementById("title");
const director = document.getElementById("director");
const url = document.getElementById("url");
let informations = [title.value, director.value, url.value];
if (informations.every((values) => values != "")) {
const createdFilm = new FILM(title.value, director.value, url.value);
UI.addFilmToUI(createdFilm);
LS.addItemsToLocalStorage([title.value,director.value,url.value]);
} else{
alert("Cant be empty!")
}
UI.clearInputs(title,director,url)
}
When I refresh my page the only thing I get is undefined.
When i upload only film the console output is:
[Array(3)]
0: (3) ["Avatar", "James Cameron", "js.png"]
length: 1
proto: Array(0)
Ah, I found my mistake. I send multiple params to a single-valued param function also when I calling my data from storage, I did kind of override I guess. I tried to invoke my data from where I put them into UI. That's why I confused. Therefore I created one function more which called loadAllData().
And also i adjust my eventListener function as well.
- LS.addItemsToLocalStorage(createdFilm)
document.addEventListener("DOMContentLoaded",function(){
let films = LS.getAllItemsFromLocalStorage();
UI.loadAllItems(films)
})
static loadAllItems(films){
const filmList = document.getElementById("films");
films.forEach((film)=>{
filmList.innerHTML += ` <tr>
<td><img src="${film.url}" class="img-fluid img-thumbnail"></td>
<td>${film.title}</td>
<td>${film.director}</td>
<td>Filmi Sil</td>
</tr>`
})
}

Highlighting when HTML and Xpath is given

Given the HTML as a string, the Xpath and offsets. I need to highlight the word.
In the below case I need to highlight Child 1
HTML text:
<html>
<body>
<h2>Children</h2>Joe has three kids:<br/>
<ul>
<li>
Child 1 name
</li>
<li>kid2</li>
<li>kid3</li>
</ul>
</body>
</html>
XPATH as : /html/body/ul/li[1]/a[1]
Offsets: 0,7
Render - I am using react in my app.
The below is what I have done so far.
public render(){
let htmlText = //The string above
let doc = new DOMParser().parseFromString(htmlRender,'text/html');
let ele = doc.evaluate("/html/body/ul/li[1]/a[1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); //This gives the node itself
let spanNode = document.createElement("span");
spanNode.className = "highlight";
spanNode.appendChild(ele);
// Wrapping the above node in a span class will add the highlights to that div
//At this point I don't know how to append this span to the HTML String
return(
<h5> Display html data </h5>
<div dangerouslySetInnerHTML={{__html: htmlText}} />
)
I want to avoid using jquery. Want to do in Javascript(React too) if possible!
Edit:
So if you notice the Render function it is using dangerouslySetHTML.
My problem is I am not able manipulate that string which is rendered.
This is what I ended up doing.
public render(){
let htmlText = //The string above
let doc = new DOMParser().parseFromString(htmlRender,'text/html');
let xpathNode = doc.evaluate("/html/body/ul/li[1]/a[1]", doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const highlightedNode = xpathNode.singleNodeValue.innerText;
const textValuePrev = highlightedNode.slice(0, char_start);
const textValueAfter = highlightedNode.slice(char_end, highlightedNode.length);
xpathNode.singleNodeValue.innerHTML = `${textValuePrev}
<span class='pt-tag'>
${highlightedNode.slice(char_start, char_end)}
</span> ${textValueAfter}`;
return(
<h5> Display html data </h5>
<div dangerouslySetInnerHTML={{__html: doc.body.outerHTML}} />
)
Xpath is inherently cross component, and React components shouldn't know much about each other. Xpath also basically requires all of the DOM to be created in order to query it. I would render your component first, then simply mutate the rendered output in the DOM using the Xpath selector.
https://jsfiddle.net/69z2wepo/73860/
var HighlightXpath = React.createClass({
componentDidMount() {
let el = document.evaluate(this.props.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
el.singleNodeValue.style.background = 'pink';
},
render: function() {
return this.props.children;
}
});
Usage:
<HighlightXpath xpath="html//body//div/p/span">
... app ...
</HighlightXpath>

TagHelper child tag unescaped attribute value

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);

Categories