Svelte each block rendering only the last item - javascript

I'm trying to make a tag input component in svelte and I've encountered a strange #each block behavior. I have the following svelte code
<script>
import Input from './Input.svelte'
import Tag from './Tag.svelte'
export let tagValues = []
let currentTag = ''
function handleEnter({ key }) {
if (key === 'Enter' && currentTag) {
if (tagValues.indexOf(currentTag) < 0) {
tagValues = [...tagValues, currentTag]
}
currentTag = ''
}
}
</script>
<div class="relative">
<Input bind:value={currentTag} on:keyup={handleEnter}></Input>
{#each tagValues as t (t)}
<Tag value={t}></Tag>
{/each}
{JSON.stringify(tagValues, null, 2)}
</div>
The idea is to get the input value when the user hits Enter key and add that value as a tag if it isn't present already. Input component behaves the same as input Html element. Tag component only has a value property which is text, this should render all the values of the tagValues array, but only shows the last one. I can confirm that the array includes correctly new tags, because I pasted a stringify version of it to HTML. This is how it looks with 1 tag
And this is with two tags
How can I make this component to render all tags? I have tried to make my each block keyed but nothing changed.

Related

How can you select code nodes that were added through templates?

So I am using the template method to dynamically add HTML content in a page, and I want to change the value of an input through an event listener.
Here's a completely random snippet of code as an example (it's nonsensical on purpose):
favoriteElement += `<div class="favorite__page JS-favoritePage">
<p id="JS-amountOfFavorites">Quantity of saved pages : ${amount}</p>
<input type="number" class="favoritesQuantity" name="amountOfFavorites" min="1" max="100" value="${value}">
</div>`
So let's say that I want to have access to the value of the input, I'll declare a variable and get it through their query selector :
let inputFavoritesQuantity = document.querySelector('input [class="favoritesQuantity"]');
Now I'll add an event listener:
inputFavoritesQuantity.addEventListener("input", function(e){
let valueOfInput = e.target.value;
//Other code
}
Though the problem is that I do not have access to the input because it's added with a template, so it gives an error Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
I could add everything by hand using the properties createElement,setAttribute,appendChild...
But it would make the code VERY long and difficult to maintain! (without even considering the fact on my code project I'd have to add 5 nested elements which have 5 attributes each!)
Is there another efficient method to have access to an element with templates?
The DOMParser compiles strings into a document. You need to access the
documentElement in order to add to the existing dom. Here's an example of use
let amount = 100
let value = 50
favoriteElement = `<div class="favorite__page JS-favoritePage">
<p id="JS-amountOfFavorites">Quantity of saved pages : ${amount}</p>
<input type="number" name="amountOfFavorites" min="1" max="100" value="${value}" />
</div>`
// This converts the string and gets the documentElement.
var node = new DOMParser().parseFromString(favoriteElement, "text/html").documentElement
//Now we are working with an actual element and not a string of text.
let inputFavoritesQuantity = node.querySelector('input [class="favoritesQuantity"]');
node.addEventListener("input", function(e){
let valueOfInput = e.target.value;
console.log('value changed', valueOfInput);
})
var outputDiv = document.getElementById('content')
outputDiv.appendChild(node);
<div id="content">
</div>

svelte prop value not working with if statement

this is my svelte component code
<script>
export let canCascade = true;
let show = true;
function cascade() {
if (canCascade) {
show = !show;
}
}
</script>
{#if show}
<div class="shade" on:click|self={cascade}>
shade
</div>
{/if}
When I use the component as <Component canCascade=false /> the 'if block' doesn't work.
But hard-coding the value inside just works fine.
Am I missing something here - some conceptual error?
Like #Corrl pointed out in the comments, you need to use {brackets}. If you don't, the variable will follow the rules of an html attribute.
Working repl https://svelte.dev/repl/d13df678eab243e9a13fb705da197219?version=3
In Svelte when we want to pass JavaScript value / expression to an attribute of a component we need to wrap the value / expression with curly brackets {}.
Otherwise, it will be used as a string.
As an example, take a look at the following code:
Component.svelte:
<script>
export let test = true;
$test: console.log(`typeof test = ${typeof test}`);
</script>
App.svelte:
<script>
import Component from "./Component.svelte";
</script>
<Component test=true />
<div />
When you will open the console inside the browser developer tools,
the output will be:
typeof test = string

Saving Values to Backend from TextBoxes using React Flux Pattern

I have several text boxes and a save button
Each text box value is loaded using the following approach
{
this.getElement('test3lowerrangethreshold', 'iaSampling.iaGlobalConfiguration.test3lowerrangethreshold',
enums.IASamplingGlobalParameters.ModerationTest3LowerThreshold)
}
private getElement(elementid: string, label: string, globalparameter: enums.IASamplingGlobalParameters): JSX.Element {
let globalParameterElement =
<div className='row setting-field-row' id={elementid}><
span className='label'>{localeHelper.translate(label)}</span>
<div className="input-wrapper small">
<input className='input-field' placeholder='text' value={this.globalparameterhelper.getDataCellContent(globalparameter, this.state.globalParameterData)} />
</div>
</div>;
return globalParameterElement;
}
Helper Class
class IAGlobalParametesrHelper {
public getDataCellContent = (globalparameter: enums.IASamplingGlobalParameters, configdata: Immutable.List<ConfigurationConstant>) => {
return configdata?.find(x => x.key === globalparameter)?.value;
}
}
This works fine. Now the user is allowed to update these text values.And on click of save the changes should be reflected by calling a web api .
I have added an onlick event like this
<a href='#' className='button primary default-size' onClick={this.saveGlobalParameterData}>Save</a>
Now inorder to save the data i need a way to identify the text element which has changed.For that i have added an update method within the Helper class
public updateCellValue = (globalparameter: enums.IASamplingGlobalParameters, configdata: Immutable.List<ConfigurationConstant>,updatedvalue:string) => {
let itemIndex = configdata.findIndex(x => x.key === globalparameter);
configdata[itemIndex] = updatedvalue;
return configdata;
}
and return the updated configdata ,and i plan to call this method in the onchange event of every text box like this
<input className='input-field' placeholder='text' onchange={this.setState({ globalParameterData: this.globalparameterhelper.updateCellValue(globalparameter, this.state.globalParameterData, (document.getElementById(elementid) as HTMLInputElement).value})}
But this does not seem like a correct approach as there are number of syntactical errors. I initially got the data using an actioncreator like this.Please advice.
samplingModerationActionCreator.getGlobalParameters();
samplingModerationStore.instance.addListener(samplingModerationStore.SamplingModerationStore
.IA_GLOBAL_PARAMETER_DATA_GET_EVENT,
this.getGlobalParameterData);
}

Convert into HTML checkboxes using JSON values in react

This JSON is dynamic, coming from API and I can't change it. I want to get table.cc value and convert it into HTML checkbox.
let table = {
id: 1,
value: "abc",
height: 1080,
width: 1920,
cc: "{"c08":false,"c07":true}"
}
let headers = Object.keys(table);
let rows = Object.values(table);
let cc = table.cc ? JSON.parse(table.cc) : null;
if(cc) {
let output = Object.entries(cc).map(([key, value]) => {
return `<input type="checkbox" checked=${value}>
<label>${key}</label>`;
}).join('');
console.log(output);
rows[4] = `${output}`; // I am getting a string. I am unable to convert it into HTML markup.
}
Since I am looping over Object.values(table), I want to change only the table.cc to get HTML checkboxes. So, in the case of the above example table.cc should have 2 checkboxes in HTML and the second one should be checked since the value is true. The label should be the key.
Any ideas?
I have put a link in stackblitz to edit the code.
https://stackblitz.com/edit/react-5dtdjt
Your code is working after some fixes. You should not add html tags as strings in JSX. And a Fragment needs to be used as parent element.
import React, { Component, Fragment } from 'react';
--------
...
if (cc) {
let output = Object.entries(cc).map(([key, value]) => {
return (
<Fragment>
<input type="checkbox" checked={value} />
<label>{key}</label>
</Fragment>
)
});
rows[4] = output

Svelte.js - Can't Properly Delete Item From List

I am building an application where users can add and delete items of different "types". For example there may be a text type where users can edit the text, and an image type where users can drop in an image.
Adding an item to the list will cause the list to update and display all old items along with the new item. Similarly, deleting an item will remove that item and cause the list to only display the remaining items.
The add functionality works perfectly fine. The issue comes with deletes when the items are of different types. Here is my (simplified) code:
// List.svelte
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
type: "text",
value: "Test 1"
},
{
type: "img",
value: "https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
// Wrapper.svelte
<script>
import {onMount} from "svelte";
export let item;
export let store;
export let myIndex;
let DynamicItem;
const handleDeleteItem = () => {
store.update((items) => ([...items.slice(0, myIndex), ...items.slice(myIndex + 1)]))
}
onMount(async () => {
let imported;
if(item.type === "text") {
imported = await import("./TextItem.svelte")
} else {
imported = await import("./ImgItem.svelte")
}
DynamicItem = imported.default;
})
</script>
<svelte:component this={DynamicItem} data={item} />
<button on:click={handleDeleteItem}>Delete</button>
// TextItem.svelte
<script>
export let data;
</script>
<div contenteditable="true" bind:textContent={data.value} />
// ImgItem.svelte
<script>
export let data;
</script>
<img src="{data.value}" width="100px" height="100px" alt="img alt" />
When the user deletes the TextItem from the list by clicking on the first delete button, the ImgItem no longer displays the image and instead only renders the src attribute as text.
When looking at the HTML output, before the delete action it looks like this:
<body>
<div contenteditable="true">Test 1</div>
<button>Delete</button>
<img src="https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
And afterwards, it looks like this:
<body>
<div contenteditable="true">https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif</div>
<button>Delete</button>
</body>
So it looks like Svelte is not truly deleting the TextItem but moving the content from the next item into its place.
How can I fix my code so that items are truly deleted? The goal should be to produce the following HTML by performing the same action:
<body>
<img src="https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
Any help is appreciated. Thank you
So it looks like Svelte is not truly deleting the TextItem but moving the content from the next item into its place.
Yes, absolutely, this is what is happening.
Svelte (like React) only recreate an element in a given position when its nature has obviously changed, for example if the tag name is not the same. Otherwise, it is considered that it is the same element / component with just some props / state that has changed.
In order to hint the framework that the identity of an element has changed, you must the key functionality. In React, it's a literal key prop. In Svelte, keys are only supported in {#each ...} blocks (for now?). The syntax is the following (docs):
{#each expression as name, index (key)}...{/each}
# also works, I think it is an oversight in the docs currently
{#each expression as name (key)}...{/each}
The value of the key must be unique to each item for it to work. Typically you'd use an id of sort. But truly, it can be anything. If the value change for the element / component at a given position, then it will be recreated.
So, in your case, you could fix your issue by using the item type as a key, for example:
{#each $listStore as item, i (item.type)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
This fixes the issue you're asking about, but it's not such a great idea I must say. For other reasons, you'd better add real unique ids to your items and use them instead. With the above code, items of the same type will continue to share DOM elements, and that's probably not what you want. You may have issues with focus or internal state (value) of DOM elements like inputs, for example.
So, something like this is better and is probably the way to go:
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
id: 1,
type: "text",
value: "Test 1"
},
{
id: 2,
type: "img",
value: "https://media.giphy.com/media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i (item.id)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}

Categories