binding textContent and overriding dom with svelte - javascript

I have a div with contenteditable=true and bind:textContent={value} so it behaves pretty much like a textarea.
The only issue I have with it is that I want to override the content of the div by processing the value, but seems like it is not possible.
To test I wrote this
<div contenteditable="true" bind:textContent={value}>testVal</div>
where value is an exported property of the component.
I kind of expected value to be set to testVal, but instead the div contains the value property.
I sort of understand why this is happening and that what I am doing is sort of an edge case, but is it at all possible to change this behaviour to kind of get a one way binding to value?
and I have tried my "normal" way of creating a one way binding (with some hacks to demonstrate issues):
<div contenteditable="true" on:input={e => value = e.target.textContent}>
{#each (value || "").split("") as part}
{part}
{/each}
</div>
this looks fine, but whenever I change type in the div my input gets multiplied, i.e. if I type e the div gets updated with ee. If I add another e I get eeee

I think the way to go is to use your "normal" way of creating a one way binding. Otherwise, using multiple ways of binding on the same element will conflict.
I used a combination of on:input like you described and, inside of the div, {#html html}
The following example formats each other word in bold as you type (there's some glitch when starting with an empty field):
<script>
import {tick} from "svelte";
let html = '<p>Write some text!</p>';
// for the implementation of the two functions below, see
// https://stackoverflow.com/a/13950376/4262276
let saveSelection = (containerEl) => { /**/ };
let restoreSelection = (containerEl, savedSel) => { /**/ };
let editor;
function handleInput(e){
const savedSelection = saveSelection(editor);
html = e.target.textContent
.split(" ")
.map((t, i) => i % 2 === 0
? `<span style="font-weight:bold">${t}</span>`
: t
)
.join(" ");
tick().then(() => {
restoreSelection(editor, savedSelection);
})
}
</script>
<div
bind:this={editor}
contenteditable="true"
on:input={handleInput}
>{#html html}</div>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

Related

Conditional styling on class in Svelte

I'm trying to use Svelte to do some conditional styling and highlighting to equations. While I've been successful at applying a global static style to a class, I cannot figure out how to do this when an event occurs (like one instance of the class is hovered over).
Do I need to create a stored value (i.e. some boolean that gets set to true when a class is hovered over) to use conditional styling? Or can I write a function as in the example below that will target all instances of the class? I'm a bit unclear why targeting a class in styling requires the :global(classname) format.
App.svelte
<script>
// import Component
import Katex from "./Katex.svelte"
// math equations
const math1 = "a\\htmlClass{test}{x}^2+bx+c=0";
const math2 = "x=-\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}";
const math3 = "V=\\frac{1}{3}\\pi r^2 h";
// set up array and index for reactivity and initialize
const mathArray = [math1, math2, math3];
let index = 0;
$: math = mathArray[index];
// changeMath function for button click
function changeMath() {
// increase index
index = (index+1)%3;
}
function hoverByClass(classname,colorover,colorout="transparent")
{
var elms=document.getElementsByClassName(classname);
console.log(elms);
for(var i=0;i<elms.length;i++)
{
elms[i].onmouseover = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorover;
}
};
elms[i].onmouseout = function()
{
for(var k=0;k<elms.length;k++)
{
elms[k].style.backgroundColor=colorout;
}
};
}
}
hoverByClass("test","pink");
</script>
<h1>KaTeX svelte component demo</h1>
<h2>Inline math</h2>
Our math equation: <Katex {math}/> and it is inline.
<h2>Displayed math</h2>
Our math equation: <Katex {math} displayMode/> and it is displayed.
<h2>Reactivity</h2>
<button on:click={changeMath}>
Displaying equation {index}
</button>
<h2>Static math expression within HTML</h2>
<Katex math={"V=\\pi\\textrm{ m}^3"}/>
<style>
:global(.test) {
color: red
}
</style>
Katex.svelte
<script>
import katex from "katex";
export let math;
export let displayMode = false;
const options = {
displayMode: displayMode,
throwOnError: false,
trust: true
}
$: katexString = katex.renderToString(math, options);
</script>
<svelte:head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex#0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
</svelte:head>
{#html katexString}
If I understand it correctly you have a DOM structure with arbitrary nested elements and you would want to highlight parts of the structure that share the same class.
So you would have a structure like this:
<div>
<p>This is some text <span class="a">highlight</span></p>
<span class="a">Another highlight</span>
<ul>
<li>Some listitem</li>
<li class="a">Some listitem</li>
<li class="b">Some listitem</li>
<li class="b">Some listitem</li>
</ul>
</div>
And if you select an element with class="a" all elements should be highlighted regardles where they are in the document. This arbitrary placement makes using the sibling selector in css not possible.
There is no easy solution to this, but I will give you my attempt:
This is the full code with some explanation
<script>
import { onMount } from 'svelte'
let hash = {}
let wrapper
onMount(() => {
[...wrapper.querySelectorAll('[class]')].forEach(el => {
if (hash[el.className]) return
else hash[el.className] = [...wrapper.querySelectorAll(`[class="${el.className}"]`)]
})
Object.values(hash).forEach(nodes => {
nodes.forEach(node => {
node.addEventListener('mouseover', () => nodes.forEach(n => n.classList.add('hovered')))
node.addEventListener('mouseout', () => nodes.forEach(n => n.classList.remove('hovered')))
})
})
})
</script>
<div bind:this={wrapper}>
<p>
Blablabla <span class="a">AAA</span>
</p>
<span class="a">BBBB</span>
<ul>
<li>BBB</li>
<li class="a b">BBB</li>
<li class="b">BBB</li>
<li class="b">BBB</li>
</ul>
</div>
<style>
div :global(.hovered) {
background-color: red;
}
</style>
The first thing I did was use bind:this to get the wrapping element (in your case you would put this around the {#html katexString}, this will make that the highlight is only applied to this specific subtree.
Doing a querySelector is a complex operation, so we will gather all the related nodes in a sort of hashtable during onMount (this kind of assumes the content will never change, but since it's rendered with #html I believe it's safe to do so).
As you can see in onMount, I am using the wrapper element to restrict the selector to this section of the page, which is a lot faster than checking the entire document and is probably what you want anyway.
I wasn't entirely sure what you want to do, but for simplicity I am just grabbing every descendant that has a class and make a hash section for each class. If you only want certain classes you could write out a bunch of selectors here instead:
hash['selector-1'] = wrapper.querySelectorAll('.selector-1');
hash['selector-2'] = wrapper.querySelectorAll('.selector-2')];
hash['selector-3'] = wrapper.querySelectorAll('.selector-3');
Once this hashtable is created, we can loop over each selector, and attach two event listeners to all of the elements for that selector. One mouseover event that will then again apply a new class to each of it's mates. And a mouseout that removes this class again.
This still means you have to add hovered class. Since the class is not used in the markup it will be removed by Svelte unless you use :global() as you found out yourself. It is indeed not that good to have global classes because you might have unintended effect elsewhere in your code, but you can however scope it as I did in the code above.
The line
div > :global(.hovered) { background-color: red; }
will be processed into
div.svelte-12345 .hovered { background-color: red; }
So the red background will only be applied to .hovered elements that are inside this specific div, without leaking all over the codebase.
Demo on REPL
Here is the same adapted to use your code and to use a document-wide querySelector instead (you could probably still restrict if wanted by having the bind one level higher and pass this node into the component)
Other demo on REPL

What would be a better way (function) to change the color of the number after onclick?

I am still fairly new to JavaScript and am trying to deepen my understanding of it through mini projects.
In this counter project, I have managed to get the result what I want:
After clicking the "add" button, the counter increment would increase and change color to green.
After clicking the "subtract" button, the counter increment would decrease and change color to red.
Below would be my JavaScript code:
//create variables to hold the HTML selectors
var counterDisplay = document.querySelector('.counter-display');
var counterMinus = document.querySelector('.counter-minus');
var counterPlus = document.querySelector('.counter-plus');
//create variable for the counter
//what we'll use to add and subtract from
var count = 0;
//create a function to avoid code redundancy
function updateDisplay(){
counterDisplay.innerHTML = count;
};
function toGreen(){
document.querySelector('.counter-display').style.color = "green";
};
function toRed(){
document.querySelector('.counter-display').style.color = "red";
};
/*-------------------------------------------*/
updateDisplay();
//EventListeners
counterPlus.addEventListener("click", () =>{
count++;
updateDisplay();
toGreen();
});
counterMinus.addEventListener("click", () =>{
count--;
updateDisplay();
toRed();
});
I separated the color functions but I feel like there's a cleaner way to write this code i.e conditionals like if statements, however, as I'm still learning I don't fully know how to implement this.
**As mentioned, I'm still learning so a thorough explanation/laymen's terms is greatly appreciated!
Please let me know for any clarifications or if more info is needed!
Thank you in advance!
EDIT:
Thank you those who have taken the time to help me in sharing their solutions!
The code you wrote is quite long but it does the job
I'm not sure what do you want exactly, but here are few notes :
Use HTML onclick Event instead:
Instead of adding event listener from javascript you can add it in the HTML code like so: <button onclick="myFunction()">Click me</button>, and whenever the button is clicked myFunction() will be called.
You can also pass the button as a parameter, for example
function myFunction(element) {
element.style.backgroundColor = "red";
}
<button onclick="myFunction(this)">Click me</button>
Use let instead of var:
Variables declared by var keyword are scoped to the immediate function body (hence the function scope) while let variables are scoped to the immediate enclosing block denoted by { } (hence the block scope).
find more info here: What's the difference between using “let” and “var”?
You need to make the following few changes:
Make use of let and const. If it doesn't change its value in the future then use const else let.
You can create an array of buttons (in the below example buttons) so that you can loop over and add event listener on all buttons. So that you don't need to add an event listener on each one of the buttons.
You can use data attribute to recognize which type of button it is.
Get rid of multiple functions toRed or toGreen, instead make a single function that will change the color of text with only one function changeTextColor.
If you just need to change the text of the HTML element then better to use textContent in updateDisplay function.
see innerText vs innerHTML vs label vs text vs textContent vs outerText
//create variables to hold the HTML selectors
const counterDisplay = document.querySelector(".counter-display");
const counterMinus = document.querySelector(".counter-minus");
const counterPlus = document.querySelector(".counter-plus");
//create variable for the counter
//what we'll use to add and subtract from
let count = 0;
//create a function to avoid code redundancy
function updateDisplay() {
counterDisplay.textContent = count;
}
function changeTextColor(color) {
counterDisplay.style.color = color;
}
/*-------------------------------------------*/
//EventListeners
const buttons = [counterMinus, counterPlus];
buttons.forEach((btn) => {
btn.addEventListener("click", (e) => {
const btnType = e.target.dataset.type;
if (btnType === "sub") {
count--;
changeTextColor("red");
} else {
count++;
changeTextColor("green");
}
updateDisplay();
});
});
.counter-display {
background-color: black;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 2rem;
font-weight: 700;
text-align: center;
color: whitesmoke;
}
.button-container {
display: flex;
gap: 1rem;
justify-content: center;
}
.button-container>* {
padding: .75rem 3rem;
font-size: 2rem;
border: none;
border-radius: 4px;
}
.counter-minus {
background-color: red;
}
.counter-plus {
background-color: green;
}
<div class="counter-display"> 0 </div>
<div class="button-container">
<button class="counter-minus" data-type="sub">-</button>
<button class="counter-plus" data-type="add">+</button>
</div>
How about something more app based? You can break your whole thing down into a couple key components with a little glue get something that you can easily build on. Its not very different than what you already have, it just sprinkles in some tried and true patterns.
const widget = (element) => {
const $minus = element.querySelector('.counter-minus');
const $plus = element.querySelector('.counter-plus');
const $display = element.querySelector('.counter-display');
let state = {
color: 'black',
count: 0
};
const update = (next) => {
state = next;
render();
};
const render = () => {
$display.innerText = state.count;
$display.style.color = state.color;
};
const onMinusClick = () => {
update({ color: 'red', count: state.count - 1 });
};
const onPlusClick = () => {
update({ color: 'green', count: state.count + 1 });
};
$minus.addEventListener('click', onMinusClick);
$plus.addEventListener('click', onPlusClick);
render();
return () => {
$minus.removeEventListener('click', onMinusClick);
$plus.removeEventListener('click', onPlusClick);
};
};
widget(document.querySelector('#app1'));
widget(document.querySelector('#app2'));
widget(document.querySelector('#app3'));
div {
margin-bottom: .5rem;
}
<div id="app1">
<button class="counter-minus">-</button>
<span class="counter-display"></span>
<button class="counter-plus">+</button>
</div>
<div id="app2">
<button class="counter-minus">-</button>
<span class="counter-display"></span>
<button class="counter-plus">+</button>
</div>
<div id="app3">
<button class="counter-minus">-</button>
<span class="counter-display"></span>
<button class="counter-plus">+</button>
</div>
First thing you probably notice is that i have multiple instance running of my app. I can do this because I dont rely on global state. You can see that when you call widget, it holds its own variables.
I have state as an object and the only thing that can write to state is the update function. You had something very similar except i combined my state into a single variable and have 1 function in charge of writing to state, and then reacting to it i.e. rendering.
Then I have some event handlers being attached that are just calling update with how they want the state to look. This of course triggers a render to keep the ui consistent with the state. This 1 way data flow is very important to keep code clear. The update function can be more sophistacted and do things like except partial state values and spread state = { ...state, ...next}. it can get pretty crazy.
Lasty i return a function. You can return anything in here, you might want to return an api for a user to interact with you widget with. For example you have a calendar and you want to be able to set the date outside of the widget. You might return { setDate: (date) => update({ date }) } or something. However i return a function that will clean up after the widgets by removing event listeners so garbage collection can reclaim any memory i was using.

Function Return HTML

I have function who return html
renderSuggestion(suggestion) {
const query = this.query;
if (suggestion.name === "hotels") {
const image = suggestion.item;
return this.$createElement('div', image.title);
} else {
let str = suggestion.item.name;
let substr = query;
return this.$createElement('div', str.replace(substr, `<b>${substr}</b>`));
}
},
But<b> element not render in browser as html element. Its display like string...
How I display this <b> element?
Tnx
That is because when you provide a string as the second argument of createElement, VueJS actually inserts the string as a text node (hence your HTML tags will appear as-is). What you want is actually to use a data object as the second argument, which give you finer control over the properties of the created element:
this.$createElement('div', {
domProps: {
innerHHTML: str.replace(substr, `<b>${substr}</b>`)
}
});
Of course, when you are using innerHTML, use it with caution and never insert user-provided HTML, to avoid XSS attacks.
You can also create a component and use v-html to render the output.
Declare props for your inputs:
export default {
props: {
suggestion: Object,
query: String
}
};
And use a template that uses your logic in the template part
<template>
<div class="hello">
<div v-if="suggestion.name === 'hotels'">{{suggestion.item.title}}</div>
<div v-else>
<div v-html="suggestion.item.name.replace(this.query, `<b>${this.query}</b>`)"/>
</div>
</div>
</template>
This allows for greater flexibility when using more complex layouts.
A working example here
Provide more detail(possibly a picture) of how it's not showing. Consider using a custom CSS class to see the div and what's happening to it.
bold {
border-style: black;
font-weight: bold;
}
then just use the "bold" class instead of "b".

onClick remove borders and on next click add them again CSS,Preact

Hello using a table kind of like this
https://jsfiddle.net/vw19pbfo/24/
how could i make a trigger onClick that removes borders on first click and on second click add them back but that should only happen on the row that is being clicked on and not affect the other. I have tried to have a conditional css on the first and last <td> but that affected every border but i only want to affect the clicked one
function removeBorders(e){
var target = e.target || e.srcElement;
target.parentElement.classList.toggle('without-border');
};
Working JSFiddle: https://jsfiddle.net/andrewincontact/su86fhxo/9/
Changes:
1) to css:
.my-table-row.without-border td {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
2) to html:
onclick=removeBorders(event) instead onClick=this.removeBorders()
One way would be to check the <td> element's parent and add/remove a custom class, like so:
function removeBorders(e) {
var row = e.parentElement;
if (row.className.indexOf("has-borders") === -1) {
row.classList.add("has-borders");
} else {
row.classList.remove("has-borders");
}
};
Working JSFiddle: https://jsfiddle.net/d380sjrh/
I also changed onClick=this.removeBorders() to onclick="removeBorders(this);".
JSFiddle: https://jsfiddle.net/4qdstec7/
Use React's state to set and unset the selected class.
const Row = ({ children }) => {
const [selected, setSelected] = useState(false);
const onClick = e => setSelected(!selected);
return (
<tr
className={selected && 'selected'}
onClick={onClick}
>
{children}
</tr>
)
}
Developing in React requires a shift in thinking from traditional web development. Please take some time to read this excellent post from the React team.

Can you pass an element to a function within the template in Vue?

I'm trying to calculate and set an element's max-height style programmatically based on the number of children it has. I have to do this on four separate elements, each with a different number of children, so I can't just create a single computed property. I already have the logic to calculate the max-height in the function, but I'm unable to pass an element from the template into a function.
I've tried the following solutions with no luck:
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
This didn't work because $refs is not yet defined at the time I'm passing it into the function.
Trying to pass this or $event.target to getMaxHeight(). This didn't work either because this doesn't refer to the current element, and there was no event since I'm not in a v-on event handler.
The only other solution I can think of is creating four computed properties that each call getMaxHeight() with the $ref, but if I can handle it from a single function called with different params, it would be easier to maintain. If possible, I would like to pass the element itself from the template. Does anyone know of a way to do this, or a more elegant approach to solving this problem?
A cheap trick I learned with Vue is that if you require anything in the template that isnt loaded when the template is mounted is to just put a template with a v-if on it:
<template v-if="$refs">
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
</template>
around it. This might look dirty at first, but the thing is, it does the job without loads of extra code and time spend and prevents the errors.
Also, a small improvement in code length on your expandable-function:
const expandable = el => el.style.maxHeight =
( el.classList.contains('expanded') ?
el.children.map(c=>c.scrollHeight).reduce((h1,h2)=>h1+h2)
: 0 ) + 'px';
I ended up creating a directive like was suggested. It tries to expand/compress when:
It's clicked
Its classes change
The element or its children update
Vue component:
<button #click="toggleAccordion($event.currentTarget.nextElementSibling)"></button>
<div #click="toggleAccordion($event.currentTarget)" v-accordion-toggle>
<myComponent v-for="data in dataList" :data="data"></myComponent>
</div>
.....
private toggleAccordion(elem: HTMLElement): void {
elem.classList.toggle("expanded");
}
Directive: Accordion.ts
const expandable = (el: HTMLElement) => el.style.maxHeight = (el.classList.contains("expanded") ?
[...el.children].map(c => c.scrollHeight).reduce((h1, h2) => h1 + h2) : "0") + "px";
Vue.directive("accordion-toggle", {
bind: (el: HTMLElement, binding: any, vnode: any) => {
el.onclick = ($event: any) => {
expandable($event.currentTarget) ; // When the element is clicked
};
// If the classes on the elem change, like another button adding .expanded class
const observer = new MutationObserver(() => expandable(el));
observer.observe(el, {
attributes: true,
attributeFilter: ["class"],
});
},
componentUpdated: (el: HTMLElement) => {
expandable(el); // When the component (or its children) update
}
});
Making a custom directive that operates directly on the div element would probably be your best shot. You could create a directive component like:
export default {
name: 'maxheight',
bind(el) {
const numberOfChildren = el.children.length;
// rest of your max height logic here
el.style.maxHeight = '100px';
}
}
Then just make sure to import the directive in the file you plan on using it, and add it to your div element:
<div ref="div1" maxheight></div>

Categories

Resources