Yesterday I was trying to translate a problem I had to solve in React, to Svelte, and I can't figure it out.
The problem is the following:
I have 3 inputs each one of them holds a percentage.
The 3 percentages altogether cannot add more than 100.
I have a fourth input, it's disabled, so it just shows the remaining percentage to 100%
In react is fairly easy, declare a function that takes, the event and a variable to know which input I'm taking the event from. Do the proper validations and done.
Sadly in svelte I have almost 0 experience, and I don't know how to tackle it.
This the code so far (spoiler alert it does not even get close to do what is supposed to do).
Svelte REPL
Running a console.log to show the value of sp1, inside the function that validates it, and outside the function (before and after the function), shows what I do expect:
Before the function (before): value in the input
Inside the function: value validated
Outside the function (after): value validated
So the validation and assignment of the correct value takes place, nonetheless the input keep showing wrong value (ex: input shows 112 and the value should be 100).
A more dynamic way to do this would be using arrays.
let inputs = [{
value: 0,
color: 'GRAY'
}, {
value: 0,
color: 'DARKSLATEGRAY'
}, {
value: 0,
color: 'TEAL'
}];
This would also make calculations easier since we can use the array methods. It would also enable us to use each blocks which remove redundant code. Then we will use a function called validate and pass the index of the current input to it. Using the input we can calculate the maximum possible value that can be entered in that input and set the input if it crosses that maximum value. This validate function will be called from the template on the input event.
Here's the code.
<script>
let total = 100;
let inputs = [{
value: 0,
color: 'GRAY'
}, {
value: 0,
color: 'DARKSLATEGRAY'
}, {
value: 0,
color: 'TEAL'
}];
$: spOthers = total - inputs.map(x => x.value || 0).reduce((a, b) => a + b);
function validate(index) {
let maxInput = total - inputs.map(x => x.value).filter((x, i) => i !== index).reduce((a, b) => a + b);
if (inputs[index].value > maxInput) {
inputs[index].value = maxInput;
}
}
</script>
{#each inputs as {value, color}, i}
<div class='input-container'>
<div class='square' style="background-color: {color}" />
<input type="number" min="0" class='input' bind:value={value} on:input={() => validate(i)} />
</div>
{/each}
<div class='input-container'>
<div class='square' style="background-color: DARKORANGE" />
<input type='number' class='input input-others' bind:value={spOthers} disabled/>
</div>
Note: I have omitted the styles above as there are no changes in them.
Here is a working REPL.
Nice. You can also do:
$: spOthers = inputs.reduce((a, c, i, inputs) => a + c.value, 0);
function validate(index) {
const delta = spOthers - total;
if (delta > 0) {
inputs[index].value -= delta;
}
}
Related
Let's say I have the following array in an angular controller:
somelist = [
{ name: 'John', dirty: false },
{ name: 'Max', dirty: false },
{ name: 'Betty', dirty: false }
];
I want to ng-repeat through it in my view, and generate editable fields for each record:
<div ng-repeat="i in somelist">
<input type="text" ng-model="i.name"/>
</div>
How would I go about efficiently marking the field as dirty if someone edits the textbox(model)?
I realize that I could use ng-change on the text field, however, that fires every time a user makes a single change(enters a key) on the textbox, making loads of calls unnecessarily.. Is there a more efficient way of doing this which I am missing?
With JavaScript...
*Edited: if that textareas don't have any other 'change' event to run, you can try inline onchange event, and replace it's value after have run once. Just making onchange="once(this)" become into this — onchange="" *In background. The code will still stay in your HTML. Demo:
(also exist input and keyup events... in Angular as well)
function once(e){
e.style.color="red";
e.onchange = "";
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
}
<textarea class="moo" onchange="once(this)">Change me!</textarea>
<textarea class="moo" onchange="once(this)">Me too!</textarea>
<textarea class="moo" onchange="once(this)">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
Running eventListener function once (not really):
let once = [];//creating empty array
const moo = document.getElementsByClassName('moo');//getting all textareas
for(let i = 0; i < moo.length; i++ ){//looping, to add 'change' event to each element
once.push(1);//adding '1' to array 'i' times. Here it will look like [1,1,1];
moo[i].addEventListener('change', function(){
if(once[i]==0){return}//if array element equals 0 = return and don't run the function
this.style.color = "red";
once[i] = 0;//after triggered = making array element = 0;
//just demo... remove this.
const d = document.getElementById('demo');
d.innerText = Number(d.innerText) + 1;
});
}
<textarea class="moo">Change me!</textarea>
<textarea class="moo">Me too!</textarea>
<textarea class="moo">And me!</textarea>
<br><br>
Triggered times: <span id="demo">0</span>
*function is still working each time... but returning immediately, which is better, than "full" run.
To improve efficiency, reduce the number of watchers by using :: and eliminating two-way binds, e.g.ng-model:
<div ng-repeat="i in ::somelist">
<input type="text" value="{{i.name}}"
ng-blur="$emit('nameChanged', i)"/>
</div>
Then, in your controller:
$scope.$on('nameChanged', (event, i) => updateName(i));
Then a quick, simple function that updates the name with the corresponding ID using i.id and i.name, assuming you have:
$scope.someList = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Max' },
{ id: 3, name: 'Betty }'
Explanation
If people aren't going to be added/removed from the list, then you can use :: on someList, also known as a one-time binding, to improve efficiency. This circumvents setting up a watcher.
Also, by setting value={{i.name}}, you effectively set up a one-way bind from the controller to the DOM, rather than two-way, meaning that the value of the input isn't being checked every loop of the $digest cycle, but any changes to the model will update the DOM.
Just an idea, feel free to play with variations, such as dropping blur and using a single button that would update all changed fields at once.
You won't get much more efficient than that, unless you also remove the watcher from value="{{i.name}}" like so value="{{::i.name}}", and then you manually update the DOM when the event is received.
I have an input element that bind with v-model to a item value, I want to limit the user input to just type numeric value on range between 0 to 10, I was tried this thing before(add #input and check the input value to keep it in range)
my code is like this:
<v-text-field #input="checkItem" v-model="item"></v-text-field>
checkItem(val) {
if(parseInt(val) < 1) {
this.item = 1;
} else if(parseInt(val) >10) {
this.item = 10;
}
}
Problem
after first time we type number out of range the function works great and keep it in range but when we type out of range number again the element didn't update because the new item value is the same as the old item value! to solve this I try to use forceUpdate and the $forceUpdate() not work!!!
for example
if user type anything between range number into input, because it's in range everything is ok;
but if user type 0 or any number outside of range, on the first time item value change to 1 if value was under 1 but if again type any negative value because the item value last time was changed to 1 when we set it to 1 again nothing happening on the view and the element value was not updated.
The main question is how to force vue to update this input field value?
<div><input type="number" v-model="item"></input></div>
</template>
<script>
export default {
name: "ranges",
data() {
return {
item: Number,
error: String
};
},
watch: {
item(newVal, lastVal) {
if (newVal > 10) this.item = 10
if (newVal < 1) this.item = 1
}
}
};
</script>
Here using the watcher you can do that validation
The only way to force the reactivity when the the final result is always the same, is to re-render the component for it to reflect the changes by updating its key.
Reference link here.
I have forked the sample Vue project from mdiaz00147, and modify into
this, and I think it works as the author intended it to be.
Solution Code modified from mdiaz00147 's code snippet
<template>
<div>
<input :key="inputKey" v-model="item" #change="checkItem" />
</div>
</template>
<script>
export default {
name: "ranges",
data() {
return {
item: null,
inputKey: 0,
};
},
methods: {
checkItem() {
if (parseInt(this.item) < 1) {
this.item = 1;
} else if (parseInt(this.item) > 10) {
this.item = 10;
}
this.inputKey += 1;
},
},
};
</script>
I'm using cleave.js for a date input field in my Vue.js project.
The option that I passed was this:
<cleave :options="{
date: true,
datePattern: ['m', 'd','Y']
}" id="date-input" placeholder="MM/DD/YYYY" type="text"></cleave>
How do I set maximum value for Y ?
Cleave is an input formatting library and really nothing more. It's up to you to determine how you want to limit user input. Fortunately it offers an api for accessing the underlining raw input value. The API can be found here.
Youll need to write an event listener to check the users input and cap the value as you need.
I did that for card security number and working fine
const cardCCV = new Cleave("#cardCCV", {
numeral: true,
stripLeadingZeroes: false,
onValueChanged: function (e) {
const maxSize = 3;
if (e.target.rawValue.length > maxSize) {
cardCCV.setRawValue(e.target.rawValue.substring(0, maxSize));
}
},
});
I'm trying to change number stored in a variable by clicking a button but the first time I click the button, it doesn't change the value of the variable but the second one does. I change the number in increments of 1, so when I click the button its currentNumber += 1 and I run a console.log after it to see if it changes. The first time I click it, it prints the default value, and the second time that I click it is when it actually changes, and it's messing up the intended functionality of my code. I'm using React for this.
constructor(props) {
super(props);
this.state = {
currentSize: parseInt(this.props.size),
min: 4,
max: 40,
}
};
increaseSize(){
this.setState({currentSize: this.state.currentSize + 1}, function(){
if(this.state.currentSize >= this.state.max){
this.state.currentSize = this.state.max;
this.state.isRed = true;
} else if(this.state.currentSize < this.state.max){
this.state.isRed = false;
}
});
console.log(this.state.currentSize);
};
render() {
var isBold = this.state.bold ? 'normal' : 'bold';
var currentSize = this.state.currentSize;
var textColor = this.state.isRed ? 'red' : 'black';
return(
<div>
<button id="decreaseButton" hidden='true' onClick={this.decreaseSize.bind(this)}>-</button>
<span id="fontSizeSpan" hidden='true' style={{color: textColor}}>{currentSize}</span>
<button id="increaseButton" hidden='true' onClick={this.increaseSize.bind(this)}>+</button>
<span id="textSpan" style={{fontWeight: isBold, fontSize: currentSize}} onClick={this.showElements.bind(this)}>{this.props.text}</span>
</div>
);
}
The number in the variable is then displayed but the one being displayed has a different value to the one inside the variable
As you can see in the picture, the number displayed is 26 but in the variable its 25.
Additionally, you can see that I set a min and max value for the counter. When it reaches either value, it goes 1 further in the display, but not in the console. So in the display it stops at 3 and 41 but in the console it stops at 4 and 40.
What am I doing wrong?
edit: the default value is 16, and that's whats printed to the console the first time I click the button, which is why its not working properly.
Use the functional version of setState() in order to get a hold of prev state values -- because React handles state changes asynchronously, you can't guarantee their values when you set them; This is also the cause where you are using the console.log. Instead go for something like:
increaseSize(){
const aboveMax = this.state.currentSize >= this.state.max;
this.setState( prevState => ({
currentSize: aboveMax ? prevState.max : prevState.currentSize + 1,
isRed: aboveMax
})
, () => console.log(this.state.currentSize) );
};
Or, move the console statement to the render() method if you don't want to the setState() callback function.
See https://reactjs.org/docs/react-component.html#setstate
Don't forget to set isRed in your constructor as well :)
I'm using a noUiSlider with one handle and a range from 0 to 50. I'm trying to show the increasing value of the slider on an input field to the left of the slider and the diminishing value on the right - as in the below example:
Slider
Has anyone else tried to do this or know how it can be achieved?
var connectSlider = document.getElementById('slider');
noUiSlider.create(connectSlider, {
start: 20,
tooltips: true,
decimals: 0,
connect: [true, false],
range: {
'min': 0,
'max': 50,
},
format: wNumb ({decimals:0})
});
var inputFormat = document.getElementById('slider-value');
sliderFormat.noUiSlider.on('update', function( values, handle ) {
inputFormat.value = values[handle];
});
inputFormat.addEventListener('change', function(){
sliderFormat.noUiSlider.set(this.value);
});
$("#slider").Link('lower').to('#slider-value',function(value){
$(this).val(Math.abs(value));
});
Here it is in jsfiddle
I managed to get the left value to display on the screen but don't know how to bind it to the input field. And I don't know how to get the value on the right. I assume this should be 'max value' minus 'slider value' but don't have enough knowledge of JavaScript so I would really appreciate some help with this :)
I'm working with Slider control from the 0.99.0 Materialize version and I wanted to get that behavior too. So I do some tricks as follow:
My HTML inside a form tag:
<div class="input-field col s12">
<label for="wmAttendance">Porcentaje de Asistencias</label><br>
<p class="range-field">
<input type="range" id="test5" min="0" max="100" oninput="wAttendance.value = test5.value" />
<output id="wAttendance" name="wAttendance">0<span>% - Mujeres</span></output>
<output id="mAttendance" name="mAttendance" class="right">0<span>% - Hombres</span></output>
</p>
</div>
JS needed to intercept changes on the range input
$(document).ready(function() {
$("#test5").on("input", function() {
var women = this.value, men = 100 - women;
$("#a_women").html(women);
$("#a_men").html(men);
$("#wAttendance").html(women + "<span>% - Mujeres</span>");
$("#mAttendance").html(men + "<span>% - Hombres</span>");
});
});
And voila we have make the magic happen and the result is something like this:
In this way we can catch both values to send in the form or to make some operations etc. Here you have my pen for reference.
I hope it could be useful for somebody else.
Ive had a play with you code and got this working:
var connectSlider = document.getElementById('slider');
// add max amount variable (used to calculate #slider-value-after input value)
var maxAmount = 50
noUiSlider.create(connectSlider, {
start: 20,
tooltips: true,
decimals: 0,
connect: [true, false],
range: {
'min': 0,
'max': maxAmount, // use max amount variable to set max range amount
},
format: wNumb ({decimals:0})
});
var inputFormat = document.getElementById('slider-value');
// created variable for #slider-value-after input
var inputFormat2 = document.getElementById('slider-value-after');
// for some reason you were looking for a 'sliderFormat' variable to watch for slider updates
// this wasnt created and was causing JS errors in the fiddle
// Ive updated this to the 'connectSlider' variable you created.
connectSlider.noUiSlider.on('update', function( values, handle ) {
// on update set first input value
inputFormat.value = values[handle];
// also set #slider-value-after input value by minus'ing the max value by current slider value
inputFormat2.value = maxAmount - values[handle];
});
inputFormat.addEventListener('change', function(){
connectSlider.noUiSlider.set(this.value);
});
I think it is also possible to get slider options which would remove the need to set the maxAmount variable as you could query the sliders max range amount instead but I cant remember the code for this.