Svelte each worked twice / each binding error ( Cannot set properties of undefined ) - binding

I'm trying to bind elements in {#each} block and removing them by click.
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, },
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
In my code i got 2 problems:
{#each} makes twice more iterations than he should do
after removing item in array by clicking on binded element - svelte fires an error "Cannot set properties of undefined"
Why it works like this?

I'm writing this not for asking help, but for people, that will meet same problems
Both of these problems have the same origin, so I gather them here:
some times {#each} block works twice more than expected
some times {#each} block binding throw an error
All code tested in Svelte compiler version 3.44.1
Problem 1 - each worked twice
What do I mean?
each block makes all iterations twice
Just let me show you, look at code below, how many iterations {#each} will do?
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
</script>
{#each foodList as {icon}}
<div> {icon} </div>
{/each}
If your answer is - 3, then congrats, you are right.
Ok, now we need to bind elements, that we are rendering in {#each} block.
It's the same code, just added prop 'elem' to objects for each div binding inside {#each}
Look below and try again, how many iterations will make {#each}?
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
</script>
{#each foodList as {icon, elem} }
<div
bind:this={elem}
>
{icon}
</div>
{/each}
Right... we got 6 iterations, twice more.
You can see it by adding some console.log() at first {#each} block code, like this:
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
>
{icon}
</div>
{/each}
It happens because we used same array for {#each} iterations and binding.
If we will create new array for binding - the problem will be gone:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
const elems = [];
</script>
{#each foodList as {icon}, index }
{ console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
>
{icon}
</div>
{/each}
Yeah... now the problem is gone and we got 3 iterations, as we expected.
It's a bug that lives for a long time, as I tried to find out - at least 1 year. This leads to different problems in different circumstances as out code become more complex.
Problem 2 - each binding throws error after iterable array has been sliced
(Something like: 'Cannot set properties of undefined')
What do I mean?
each block binding throws error after we removed one of iterable array items
Same example, just added removing array item on it's element click:
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
on:click={remove}
>
{icon}
</div>
{/each}
We expect that if we make a click on div - array item, to which it was bound will be sliced and we will lose them on screen. Correct... but we got error, because {#each} block make still 3 iterations, not 2 as we were waiting for.
Again for clearness, our code steps: foodList length is 3 -> we make a click on food icon -> foodList length is 2 (we cut item with clicked icon).
After this {#each} should do 2 iterations to render each left icon in foodList, but he did 3!
That's why we have the problem, our code trying to write new property to undefined (item is sliced so when we are trying to read\write it - there is undefined.
// foodList [ { icon: '🍲', elem: null, }, { icon: '🥫', elem: null, }]
foodList[2].elem = <div>; // "Cannot set properties of undefined"
It's a bug and it happens if we used same array for {#each} iterations and binding.
The most clean fix on my question is to separate iterable and binding data into different arrays:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
But... Let's look inside out new elems array by adding $: console.log(elems);
(it's reactive expression, that will print elems array each time as it changes)
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
$: console.log(elems);
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Looks like a have 2 news for you
we got no error
we have new null item in elems array
It means that problem is still here( {#each} block makes still 1 extra iteration for sliced item).
For now we can filter elems array after foodList slicing, just do it after page update, such as tick().
Full code:
<script>
import { tick } from 'svelte';
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
let elems = [];
const remove = async (index) => {
foodList.splice(index, 1);
foodList = foodList;
await tick();
elems = elems.filter((elem) => (elem !== null));
};
$: console.log(elems);
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Keep in mind: {#each} block still works 1 extra time and we got null as bound elem, we just filtered it after the page updates.
Last stand
Don't know what to say for real... I wasted too much time on this **** trying to figure out why my code isn't work as it should be.
I like svelte, but I don't like bugs
I really hope this little guide will helps some of you to save a lot of time.
Will be glad to your corrections, see you and don't let 🐞 win.
P.S.
Yep, it takes time, but... Never know when you will needs help, share your knowledge

Related

how to avoid key/id problems in reactjs and make props pass from parent to child?

I keep hitting a wall when trying to get the parent data passed down to the child component.
My view:
<%= react_component 'Items', { data: #items } %>
My Items component makes an ajax call, sets state, and renders Item. Leaving key={this.props.id} out of the Item instance passed into the mapping function makes it so that the component html renders to the page. But add the key in, and I get a console error: Uncaught TypeError: Cannot read property 'id' of undefined
Here's 'Items':
var Items = React.createClass({
loadItemsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
var itemNodes = this.props.data.map(function() {
return (
<Item key={this.props.id} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
My item.js.jsx component just formats each Item:
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
The React dev tools extension shows the props and state data inside Items. The children, however, are empty.
I'm aware of this, but I'm setting key with this.props.id. I'm not sure what I'm missing?
I found a couple of problems with the code you posted, in the Items component
You're rendering this.props.data while in fact this.state.data is the one being updated with the ajax request. You need to render this.state.data but get the initial value from props
The map iterator function takes an argument representing the current array element, use it to access the properties instead of using this which is undefined
The updated code should look like this
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
var Items = React.createClass({
getInitialState: function() {
return {
// for initial state use the array passed as props,
// or empty array if not passed
data: this.props.data || []
};
},
loadItemsFromServer: function() {
var data = [{
id: 1,
artist: 'abc'
}, {
id: 2,
artist: 'def'
}]
this.setState({
data: data
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
// use this.state.data not this.props.data,
// since you are updating the state with the result of the ajax request,
// you're not updating the props
var itemNodes = this.state.data.map(function(item) {
// the map iterator function takes an item as a parameter,
// which is the current element of the array (this.state.data),
// use (item) to access properties, not (this)
return (
// use key as item id, and pass all the item properties
// to the Item component with ES6 object spread syntax
<Item key={item.id} {...item} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
And here is a working example http://codepen.io/Gaafar/pen/EyyGPR?editors=0010
There are a couple of problems with your implementation.
First of all, you need to decide: Do you want to render the #items passed to the Items component from your view? Or do you want to load them asynchronous?
Because right now I get the impression you are trying to do both...
Render items passed from view
If you want to render the items from your view passed to the component, make sure it's proper json. You might need to call 'as_json' on it.
<%= react_component 'Items', { data: #items.as_json } %>
Then, in your Component, map the items to render the <Item /> components. Here is the second problem, regarding your key. You need to define the item variable to the callback function of your map function, and read the id from it:
var itemNodes = this.props.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);
});
Note, I also added the author as prop, since you are using it in your <Item /> Component.
You can remove your componentDidMount and loadItemsFromServer functions, since you are not using them.
Load items asynchronous
If you want to load the items asynchronously, like you are trying to do in your loadItemsFromServer function, first of all, pass the url from your view and remove the {data: #items} part, since you will load the items from your component, something like:
<%= react_component 'Items', { url: items_path(format: :json) } %>
If you want to render the asynchronous fetched items, use:
var itemNodes = this.state.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);loadItemsFromServer
});
Note I changed this.props.map to this.state.map
You can now use your componentDidMount and loadItemsFromServer functions to load the data and save them to state.

jquery mobile data-lastval

I need help with an Intel XDK project.
After research I was succesfull to create an autocomplete form.
However, if the user do not select the value from filter list,
and enter the value manually, it is not saved to the variable.
It would be possible to select the value in "data-lastval".
Here is the code that appears in DOM. But with all the research I do, I cannot understand, how to fetch this content of data-lastval and put in into a variable. On selection of the list item it goes saved to a hidden input field and can be stored to localstorage. I need help in building this javascript or jquery mobile selector, like var xstreet = ...
"If"-logic will decide, that the hidden field was empty, and then put "xstreet" instead in this.
<div class="ui-input-search ui-shadow-inset ui-input-has-clear ui-body-e ui-corner-all">
<input data-type="search" placeholder="street" data-lastval="2323">
Clear text</div>
Code in src:
<div class="widget uib_w_15 form1widths labelwhite form1forms streetfield" data-uib="jquery_mobile/input" data-ver="0" id="streetfield"><input type="hidden" id="xd">
<ul class="autocomplete" data-role="listview" data-inset="true" data-filter="true"
data-filter-placeholder="street" data-filter-theme="e"></ul>
</div>
Code in the head:
<script>
$('#streetfield ul').listview();
var gcity = window.localStorage.getItem("city");
$(document).on( "pageinit", "#form", function() {
$(".autocomplete").on("listviewbeforefilter", function(e, data ) {
var $ul=$(this);
var value = $( data.input ).val();
var dropdownContent = "" ;
$ul.html("") ;
if ( value && value.length > 2 ) {
var response = ['data1','data2','data3'];
$('.autocomplete').show();
$ul.html( "<li><div class='ui-loader'><span class='ui-icon ui-icon-loading' ></span></div></li>" );
$ul.listview( "refresh" );
$.each(response, function( index, val ) {
dropdownContent += "<li>" + val + "</li>";
$ul.html( dropdownContent );
$ul.listview( "refresh" );
$ul.trigger( "updatelayout");
});
}
});
});
$(document).on( "click", ".autocomplete li", function() {
var selectedItem = $(this).html();
$(this).parent().parent().find('input').val(selectedItem);
$('.autocomplete').hide();
});
</script>
Well, nobody helped. I think, this may be the solution.
I found it in an other case.
var x = $(#id).attr('data-lastval')
maybe it can help somebody.

give default value to md-autocomplete

How to pass default value in md-autocomplete?
Image 1 : HTML code
Image 2 : JS code
Image 3 : Output
As you can see, I am not getting any default country as output. Is there any way to do that?
Assign yr SearchText the default value & selectedItem the object.
$scope.local ={
...
searchText : 'Default Value',
selectedItem : 'Default object'
...
}
I write small codepen with autocomplete and default value.
What you must do:
Init main model.
Define model field, used in autocomplete md-selected-item property.
Define callback for loading autocomplete items.
Before save main model extract id (or other field) from associated field.
Main error in your code here:
$scope.local = {
...
selectedItem: 1, // Must be object, but not integer
...
}
(function(A) {
"use strict";
var app = A.module('app', ['ngMaterial']);
function main(
$q,
$scope,
$timeout
) {
$timeout(function() {
$scope.user = {
firstname: "Maxim",
lastname: "Dunaevsky",
group: {
id: 1,
title: "Administrator"
}
};
}, 500);
$scope.loadGroups = function(filterText) {
var d = $q.defer(),
allItems = [{
id: 1,
title: 'Administrator'
}, {
id: 2,
title: 'Manager'
}, {
id: 3,
title: 'Moderator'
}, {
id: 4,
title: 'VIP-User'
}, {
id: 5,
title: 'Standard user'
}];
$timeout(function() {
var items = [];
A.forEach(allItems, function(item) {
if (item.title.indexOf(filterText) > -1) {
items.push(item);
}
});
d.resolve(items);
}, 1000);
return d.promise;
};
}
main.$inject = [
'$q',
'$scope',
'$timeout'
];
app.controller('Main', main);
}(this.angular));
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/angular-material/0.11.0/angular-material.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular-aria.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular-animate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-material/0.11.0/angular-material.min.js"></script>
</head>
<body ng-app="app" flex layout="column" layout-margin ng-controller="Main">
<md-content layout="column" class="md-whiteframe-z1" layout-margin>
<md-toolbar>
<div class="md-toolbar-tools">
<h3>Form</h3>
</div>
</md-toolbar>
<md-content class="md-whiteframe-z1">
<div class="md-padding">
<md-input-container>
<label for="firstname">First name</label>
<input type="text" name="firstname" ng-model="user.firstname" />
</md-input-container>
<md-input-container>
<label for="lastname">Last name</label>
<input type="text" name="lastname" ng-model="user.lastname" />
</md-input-container>
<md-autocomplete md-selected-item="user.group" md-items="item in loadGroups(filterText)" md-item-text="item.title" md-search-text="filterText">
<md-item-template>{{ item.title }}</md-item-template>
<md-not-found>No items.</md-not-found>
</md-autocomplete>
</div>
</md-content>
</md-content>
<md-content class="md-whiteframe-z1" layout-margin>
<md-toolbar>
<div class="md-toolbar-tools">
<h3>Model as JSON</h3>
</div>
</md-toolbar>
<md-content class="md-padding">
<p>
{{ user | json }}
</p>
</md-content>
</md-content>
</body>
I know this is an old question, but some people may benefit from my solution. I struggled with the problem of the model for the auto-complete being asynchronous and having my autocompletes as part of an ng-repeat. Many of the solutions to this problem found on the web have only a single auto complete and static data.
My solution was to add another directive to the autocomplete with a watch on the variable that I want to set as default for the auto complete.
in my template:
<md-autocomplete initscope='{{quote["Scope"+i]}}' ng-repeat='i in [1,2,3,4,5,6,7,8]'
class='m-1'
md-selected-item="ScopeSelected"
md-clear-button="true"
md-dropdown-position="top"
md-search-text="pScopeSearch"
md-selected-item-change='selectPScope(item.label,i)'
md-items="item in scopePSearch(pScopeSearch,i)"
md-item-text="item.label"
placeholder="Plowing Scope {{i}}"
md-min-length="3"
md-menu-class="autocomplete-custom-template"
>
then in my module:
Details.directive('initscope', function () {
return function (scope, element, attrs) {
scope.$watch(function (){
return attrs.initscope;
}, function (value, oldValue) {
//console.log(attrs.initscope)
if(oldValue=="" && value!="" && !scope.initialized){
//console.log(attrs.initscope);
var item = {id:0, order:0,label:attrs.initscope?attrs.initscope:"" }
scope.ScopeSelected = item;
scope.initialized = true;
}
});
};
});
this checks for changes to the quote["Scope"+i] (because initially it would be null) and creates an initial selected item and sets the autocompletes' selected item to that object. Then it sets an initialized value to true so that this never happens again.
I used timeout to do this.
$timeout(function() {
$scope.local = {selectedItem : 1}
}, 2000);

Binding JQuery Tabs Using Knockout JS

I have ViewModel like this:
function ViewModel() {
var self = this;
self.Tab = function (id, name, text, selected) {
var tab = this;
tab.id = ko.observable(id);
tab.name = ko.observable(name);
tab.text = ko.observable(text);
return tab;
};
self.selectedTab = ko.observable(1);
self.tabs = new Array();
self.tabs.push(new self.Tab(1, 'Tab 1', 'Tab 1 Content'));
self.tabs.push(new self.Tab(2, 'Tab 2', 'Tab 2 Content'));
self.tabs.push(new self.Tab(3, 'Tab 3', 'Tab 3 Content'));
self.tabs.push(new self.Tab(4, 'Tab 4', 'Tab 4 Content'));
return self;
}
ko.applyBindings(new ViewModel(), document.getElementById("TabDiv"));
And related HTML is as follows:
<div id="TabDiv">
<div id="tabs" data-bind="foreach: tabs">
<div class="tab" data-bind="css: {selected: $parent.selectedTab() == id()}, text: name, click: $parent.selectedTab.bind($parent, id())">
</div>
</div>
<div id="tabContent" data-bind="foreach: tabs">
<div data-bind="if: $parent.selectedTab() == id()">
<span data-bind="text: text"></span>
</div>
</div>
</div>
Now,i have another ViewModel as Follows:
var ProjectViewModel = {
........
AddEmployee: function (data, event) {
$('.chkList').each(function () {
//Here i want to generate tab
});
}
};
The checkbox list is binded to one of the observable array of ProjectViewModel which is all working fine. What i am trying to do is,on click of the checkbox inside checkbox list, generate a Tab(similar to Jquery UI Tab).
You can see 4 tab values inserted statically which is working perfectly. I am getting tab as expected. But i am not able to push values which i get inside ProjectViewModel's AddEmployee function into tabs array in ViewModel. I don't know how to process self.tabs.push(new self.Tab(.....)); outside the ViewModel.
Any help is greatly appreciated.
You can use ko.dataFor() to access the view-model:
AddEmployee: function (data, event) {
var vm = ko.dataFor(document.getElementById("TabDiv"));
vm.tabs.push(new vm.Tab(5, 'New Tab', 'New Tab Content'));
}
Alternatively, you can expose the view-model to the global scope then access it everywhere:
window.tabsVM = new ViewModel();
ko.applyBindings(window.tabsVM, document.getElementById("TabDiv"));
Then:
AddEmployee: function (data, event) {
var vm = window.tabsVM;
vm.tabs.push(new vm.Tab(5, 'New Tab', 'New Tab Content'));
}
Also, you'll have to change your tabs array into an observable-array if you want your changes to update the DOM automatically:
self.tabs = ko.observableArray();

JQuery UI Autocomplete and hidden field

I have implemented the JQuery UI Autocomplete widgets in my form so when the user selects an item from the autocomplete, a hidden field is populated with the id and the textbox with the user friendly text.
I have also implemented a remote validation so if the user after selecting an item from the list (hidden field already set) decides to delete or change the text it will fail and force the user to select an item again.
I do want to allow the user to delete the field so if all content of the textbox is deleted I want to reset the hidden field, but I don't know how to do this...
Thanks in advance.
Typically, you can catch certain events on the input field that is the Autocomplete widget. The main ones would be blur where the input field loses focus and or keyup where someone has hit enter inside the input field (maybe trying to submit the form).
The following example shows how a "hidden" field could be updated in these two scenarios:
$(function() {
var availableTags = [
"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"ColdFusion",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"
];
function split( val ) {
return val.split( /,\s*/ );
}
function extractLast( term ) {
return split( term ).pop();
}
$( "#tags" )
// don't navigate away from the field on tab when selecting an item
.bind( "keydown", function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).autocomplete( "instance" ).menu.active ) {
event.preventDefault();
}
})
.autocomplete({
minLength: 0,
source: function( request, response ) {
// delegate back to autocomplete, but extract the last term
response( $.ui.autocomplete.filter(
availableTags, extractLast( request.term ) ) );
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function( event, ui ) {
var terms = split( this.value );
// remove the current input
terms.pop();
// add the selected item
terms.push( ui.item.value );
// add placeholder to get the comma-and-space at the end
terms.push( "" );
this.value = terms.join( ", " );
$("#hiddenField").val(terms.join( ", "));
return false;
}
});
});
$("#tags").blur(function() {
$("#hiddenField").val(this.value);
});
$("#tags").keyup(function (e) {
if (e.keyCode == 13) {
$("#hiddenField").val(this.value);
}
});
.hiddenFields {
margin-bottom: 30px;
}
.hiddenF {
color: gray;
}
.hiddenI {
color: gray;
border: 1px dotted gray;
opacity: 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.1/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="//code.jquery.com/ui/1.11.1/jquery-ui.js"></script>
<div class="hiddenFields">
<label for="hiddenField" class="hiddenF">Hidden Field: </label>
<input id="hiddenField" class="hiddenI" size="50" readonly/>
</div>
<div class="ui-widget">
<label for="tags">Tag programming languages: </label>
<input id="tags" size="50">
</div>

Resources