Using get and set on a data property object - VueJS - javascript

From what I know VueJS has a way to use a getter and setter for their computed properties per this documentation on Computed property.
I have here the vue component where you can see the amount is an object and we have a group of persons from the vuex store.
data() {
return {
form: {
amounts: {},
},
};
},
mounted() {
const persons = this.$store.getters.getPersons()
persons.forEach((person) => {
this.$set(this.form.amounts, person.id, '0.00');
});
},
I made it so I can associate a person to the amount he has paid on the form by linked it using the ID and the payment. This is an example of what this.form.amounts should look like.
{'HKYDUPOK': 0.00},
{'RYYJPUKA': 0.00},
{'KZMUYTAK': 0.00}
Now by default, their values should be 0.00, on the input number field where they entered the amount, by default I applied them to v-model which looks like this:
<div v-for="person in persons">
<input
class="form-control"
v-model="form.amounts[person.id]"
type="number"
step=".01"
min="0"
required>
</input>
</div>
But here is the thing, when you open your code snippet on the browser, you notice that the input number field has the default value of 0.00 which acts as somewhat a placeholder. I wanted to remove the default value of 0.00 on the number input and have it instead to an empty input yet the underlying value of the amounts per person is not null but still 0.00 or 0. This is so that the form is clear of input when the user tries to input values on the input box instead of having to erase and replace 0.00 with an actual value (Hope this is clear). Now there is a possibility that on the total amount, there are at least 1 or more persons with an amount of 0. I wanted to make sure that an empty input number field does not result in null but instead, it's 0. Is this possible?
I tried checking the computed property getter and setter for this to change the default binding yet how do you map the form.amounts to match the amount to its corresponding person? On the Get, if the value is not more than 0.00 or 0, then return an empty value to the input field. Set is the bigger problem for it only accepts one parameter which is called newValue and would be hard to say pass the personId to map the amounts to the corresponding person. Is there a way to touch upon and manipulate the binding of a data property which is an object yet also change the default behavior on the model to return empty instead of 0.00? I hope my question is clear enough.

I assume this is a follow on from your previous question...
At this stage, you're best creating a component to represent your data input element.
Something like this (using a single-file component example)
<!-- number-input.vue -->
<template>
<input class="form-control" type="number"
step=".01" min="0"
:value="amount"
#input="updated"
required />
</template>
<script>
export default {
name: 'NumberInput',
props: {
value: Number
},
computed: {
amount () {
return this.value || ''
}
},
methods: {
updated ($event) {
this.$emit('input', parseFloat($event.target.value) || 0)
}
}
}
</script>
Then you can use it in your parent template
<div v-for="person in persons" :key="person.id">
<NumberInput v-model="form.amounts[person.id]" />
</div>
Just remember to import and use the component...
<script>
import NumberInput from 'number-input'
export default {
components: { NumberInput },
// etc
}
</script>
JSFiddle Demo
Also see https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Related

StimulusJS how to compute value in parent controller from values in different child controllers (cart -> cart items)

I am building a cart with stimulus. The cart recap page displays multiple cart items. A cart item has a unit price and a quantity.
Each cart item component has a quantity selector, when the quantity number changes the total price of the cartitem element is recalculated totalCartItem = unitPrice x Quantity and the total price if the cart must also be recalculated totalCartPrice = sum(totalCartItem)
Here's a simplification of the html structure
<div class="cart" data-controller='cart'>
<div class="cart-item" data-controller='cart-item'>
<p class='unit-price'>20</p>
<input type="number" name="quantity" value="1">
<p class='total-price' data-target='cart-item.totalPrice' data-action="change->cart-item#update" data-target='cart-item.quantity'>20</p>
</div>
<div class="cart-item" data-controller='cart-item'>
<p class='unit-price'>10</p>
<input type="number" name="quantity" value="2" data-action="change->cart-item#update" data-target='cart-item.quantity'>
<p class='total-price' data-target='cart-item.totalPrice'>20</p>
</div>
<div class="cart-total">
40
</div>
</div>
My cart item controller works perfectly fine and updates correctly the totalPriceFor a cart item in the UI.
export default class extends Controller {
static targets = ["totalPrice", "quantity"]
static values = {
unitPrice: String,
}
connect() {
console.log("hello from StimulusJS")
}
update(){
this.totalPriceTarget.textContent = parseFloat(this.unitPriceValue) * parseInt(this.quantityTarget.value)
}
}
However, I am now lost on how to update the totalCartPrice. I feel like this should be the responsability of the cartController that wrapps the cartItems elements, but I have no idea on what id the correct way to achieve this.
I feel like I should add change->cart#update on each number input selector for quantity for each cart-item, but then what should I add to the cart#update method to recalculate the total from each individual cart item ?
export default class extends Controller {
static targets = ["total"]
connect() {
console.log("hello from StimulusJS")
}
update(){
// let details = event.detail;
// this.cartItemChildren.forEach((item) => console.log(item.totalValue))
}
}
As per the Stimulus documentation, it is always best to start with the HTML. By extension it is best to structure your HTML in an accessible way and see what the browser gives you without reinventing things.
When you are building forms, it is best to use the form element, even if this form is not being submitted. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
There is also a very useful output element that helps you assign an output value based on some input value. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output#attr-for
Finally, there is a DOM API for the form element that lets you access various parts of the form via their name attribute, this is form.elements. https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
Bringing this together we can start with the following HTML...
HTML
We are simply attaching our controller action to the form as a whole, so we can listen to any change to the inputs within the form.
One controller for this case, while the docs to recommend smaller granular controllers, the smallest unit content here appears to be the form as a whole.
Each input has a unique ID and each output element references the input via this id using the for attribute - this will be better for accessibility but comes in handy when we want to parse the values and update outputs.
We also have an additional data attribute on the unit price that uses the same id, note that this data attribute is nothing special but just gives us a way to 'find' the relevant price. No need to create a controller target for this simple case.
Note: This assumes you do not have to support IE11 (as the output element will not work in that browser).
<form class="cart" data-controller='cart' data-action="change->cart#updateTotals">
<div class="cart-item">
<span class='unit-price' data-price-for="cart-item-1">20</span>
x
<input type="number" name="quantity" value="1" id="cart-item-1">
=
<output class="total-price" for="cart-item-1" name="item-total">20</output>
</div>
<div class="cart-item">
<span class='unit-price' data-price-for="cart-item-2">10</span>
x
<input type="number" name="quantity" value="1" id="cart-item-2">
=
<output class="total-price" for="cart-item-2" name="item-total">10</output>
</div>
<div class="cart-total">
Total: <span data-cart-target="total">30</span>
</div>
</form>
JavaScript (Controller)
Our controller does not really need anything other than the grand total target, as we can use this.element on the base form's attached controller.
We are avoiding a generic update method and trying to be more specific with updateTotals.
We use the elements API on the form to get the elements with the name 'item-total' (note that output names do not get submitted). https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
Once we have each output element, we can find the relevant input element via the for and the input's id and then the data attribute to find the price.
We then calculate a grand total and finally update all the values in the DOM.
Note: Number parsing is very basic, best to add some safety here, also we are assuming you do not need to support IE11.
import { Controller } from '#hotwired/stimulus';
export default class extends Controller {
static targets = ['total'];
connect() {
this.updateTotals();
}
updateTotals() {
// parse the form elements to get all data
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
const cartItems = [...this.element.elements['item-total']].map(
(outputElement) => {
const forId = outputElement.getAttribute('for');
const priceElement = document.querySelector(`[data-price-for='${forId}']`);
const price = Number(priceElement.textContent);
const quantity = Number(document.getElementById(forId).value);
return { outputElement, total: quantity * price };
}
);
// calculate the grand total
const grandTotal = cartItems.reduce(
(runningTotal, { total }) => runningTotal + total,
0
);
// update the grand total
this.totalTarget.textContent = grandTotal;
// update totals for each cart item
cartItems.forEach(({ outputElement, total }) => {
outputElement.textContent = total;
});
console.log('form updated', { grandTotal, cartItems });
}
}

Form input very slow / delayed when typing

I have a form comprised of 2 objects - A and B both objects have around 20 variables in them.
When it type there is a couple of seconds delay before the state is updated with the value.
My state is along the lines of this where the data is passed in from a parent component:
export class Booking extends Component<any, BookingProps & BookingState> {
constructor(bookingProps: BookingProps) {
super(bookingProps);
this.state = {
a: bookingProps.a,
b: bookingProps.b,
errors: {}
... around six other objects for modal, default data etc
};
}
Many of the input form form fields are like this:
<Form.Field>
<label htmlFor="ref">Ref</label>
<input
placeholder="Ref"
value={this.state.a.ref}
id="ref"
onChange={this.handleBookingFieldChange}
style={{ backgroundColor: 'lightgrey' }}
/>
</Form.Field>
In the other form fields part of state.b it is exactly the same except the value is this.state.b.ref and the onChange is onChange={this.handleBookingExtrasChange}.
So essentially i have separate onChange handlers for state a and b:
private handleBookingFieldChange = (e: any) => {
const key = e.target.id;
const value = e.target.value;
this.setState({
booking: { ...this.state.a, [key]: value }
});
};
So when i type it takes around 3 seconds for the letters to appear in the text field. I'm new to React (from Angular) and can't think why this would take so long to display what i typed.
I would guess that the state is updating every field but i'm not sure. What can i do to speed it up?
I found that by switching the input a little will work as you type - essentially setting defaultValue instead ofvalue and using onBlur instead of onChange. Now the form fields update when typing:
<Form.Field>
<label htmlFor="ref">Ref</label>
<input
placeholder="Ref"
defaultValue={this.state.a.ref}
id="ref"
onBlur={this.handleBookingFieldChange}
type={'number'}
/>
</Form.Field>

Angular 2 input value not updated after reset

I have a simple input that I want to reset the value to empty string after I am adding hero. The problem is the value is not updated. why?
#Component({
selector: 'my-app',
template: `
<input type="text" [value]="name" #heroname />
<button (click)="addHero(heroname.value)">Add Hero!</button>
<ul>
<li *ngFor="let hero of heroes">
{{ hero.name }}
</li>
</ul>
`,
})
export class App {
name: string = '';
heroes = [];
addHero(name: string) {
this.heroes.push({name});
// After this code runs I expected the input to be empty
this.name = '';
}
}
You have one-way binding so when you're typing something in your input your name property isn't changed. It remains "". After clicking on Add hero! button you doesn't change it.
addHero(name: string) {
this.heroes.push({name}); // this.name at this line equals ''
this.name = ''; // it doesn't do any effect
}
Angular2 will update value property only if it is changed.
Use two-way binding which is provided by #angular/forms
[(ngModel)]="name"
to ensure that your name property will be changed after typing.
Another way is manually implementing changing
[value]="name" (change)="name = $event.target.value"
In Angular Template binding works with properties and events, not attributes. as per html attribute vs dom property documentation of angular so as you have used [value] binding its binding to attributes not to the property of that input and because of it value remain in it after you set this.name = "".

HTML placeholder override the ng-model value if it is 0

Is there a way to tell ng-model if your value is 0 then show the placeholder value not the ng-model value.
The reason for this is I have an issue where the UX requires the ng-model to be one value while the business logic requires it to be another.
Something like this.
Controller
$scope.qty = 0;
//...some calculation that requires $scope.qty to be a number
HTML
<input ng-model="qty" placeholder="N/A">
<!-- This will show 0 not N/A as qty has a value-->
I do understand you can edit the functionality of ng-model with the following.
ng-model-options={getterSetter: true}
This does solve this issue in a single ng-model like above but what about when the qty is in a ng-repeat?
In the real world we are making a page that auto calculates the amount of items a user should order based on there inventory. If the inventory is 0 the placeholder is used if it is changed by the user then the value they entered in is used.
As there is more then one product they can order we have this in an ng-repeat.
Controller
$scope.products = [{
//...product data
inventory : 0
}]
HTML
<tr ng-repeat="product in products track by $index">
<td>
<input ng-model="product.inventory" placeholder="N/A">
</td>
</tr>
You can try the setter getter method but as the only value you get is the value entered into the input box you lose the context of which item the user is referring to unless you create a setter getter function for each product.
Try this. You can define a getter function for ng-model directive if you use ng-model-options by setting getterSetter as true. ng-model-options is used to modify the behaviour of ng-model.
<input ng-model="getterSetter" placeholder="N/A" ng-model-options={getterSetter: true}>
// Controller function
$scope.getterSetter = function(value) {
if(value == 0) {
return "N/A";
}
else {
return value;
}
}
You can ngModelOptions, Object to tell Angular exactly when to update the Model.
<input ng-model="updateModel" placeholder="some text" ng-model-options={updateModel: true}>
// Controller method
$scope.updateModel= function(value) {
if(value <= 0) {
return "some text";
}
else {
return value;
}
}

translating between cents and dollars in html input in React

I'm in a bit of a weird situation, I am dealing with currency in my we app. On the model side, I am saving currency as cents before sending to the server as I don't want to deal with decimal points on the server side.
In the view however, I want the to display normal currency and not cents.
So, I have this input field where I take the data from dollars and change it to cents:
<input name="balance" type="number" step="0.01" min="0"
placeholder="Balance in cents" onChange={this.handleUpdate}
value={this.props.user.balance / 100} />
And when there's a change in the input value, I change it back to cents before sending it upstream:
handleUpdate: function(e) {
var value = e.target.value;
// changing it back from cents to dollars
value = parseFloat(value) * 100;
// save back to the parent component managing the prop
this.props.onUserUpdate(value);
}
This puts me in kind of a deadlock, there's no way for me to enter a decimal point "." Let me demonstrate :
33 in the input box --> becomes 3300 in the parent state --> goes back as 33 in component prop - all good
33.3 in the input box --> becomes 3330 in the parent state --> goes back as 33.3 in the component prop - all good
33. in the input box --> becomes 3300 in the parent state --> goes back as 33 in the component prop - this is the problem
As you can see in case #3, when the user first enters "." this doesn't translate back to the same number with "."
Since it's a controlled input, there's basically no way of writing "."
I have tried using uncontrolled element with defaultValue, but the amount prop is not ready the time the component is rendered so it's just empty
http://jsfiddle.net/fpbhu1hs/
Controlled inputs using derived values can be tricksy - if you need to be able to display invalid or otherwise weird input then...
always hold the input's value in its component's own state
<input value={this.state.value} onChange={this.handleUpdate} // rest as above...
derive the initial value in getInitialState()
getInitialState: function() {
return {value: this.props.user.balance / 100}
}
implement componentWillReceiveProps(nextProps) to detect when the prop's value is changing from above and re-derive the state value
componentWillReceiveProps: function(nextProps) {
if (this.props.user.balance != nextProps.user.balance) {
this.setState({value: nextProps.user.balance / 100})
}
}
Now when the user enters "33.", you store their literal input using setState(), then call back to the parent.
handleUpdate: function(e) {
var value = e.target.value
this.setState({value: value})
this.props.onUserUpdate(parseFloat(value) * 100)
}
If the value the parent then passes back down to the child via props hasn't changed (3300 == 3300 in this case), then componentWillReceiveProps() won't do anything.
Working snippet:
<script src="http://fb.me/react-with-addons-0.12.2.js"></script>
<script src="http://fb.me/JSXTransformer-0.12.2.js"></script>
<div id="example"></div>
<script type="text/jsx;harmony=true">void function() { 'use strict';
var Parent = React.createClass({
getInitialState() {
return {cents: 3300}
},
_changeValue() {
this.setState({cents: Math.round(Math.random() * 2000 + Math.random() * 2000)})
},
_onCentsChange(cents) {
this.setState({cents})
},
render() {
return <div>
<p><strong>Cents:</strong> {this.state.cents.toFixed(0)} <input type="button" onClick={this._changeValue} value="Change"/></p>
<Child cents={this.state.cents} onCentsChange={this._onCentsChange}/>
</div>
}
})
var Child = React.createClass({
getInitialState() {
return {dollars: this.props.cents / 100}
},
componentWillReceiveProps(nextProps) {
if (this.props.cents != nextProps.cents) {
this.setState({dollars: nextProps.cents / 100})
}
},
_onChange(e) {
var dollars = e.target.value
this.setState({dollars})
if (!isNaN(parseFloat(dollars)) && isFinite(dollars)) {
this.props.onCentsChange(parseFloat(dollars) * 100)
}
},
render() {
return <div>
<input type="number" step="0.01" min="0" value={this.state.dollars} onChange={this._onChange}/>
</div>
}
})
React.render(<Parent/>, document.querySelector('#example'))
}()</script>
I'm using this simple solution to handle controlled inputs and decimal values.
Create two props in your state, one to hold actual value and another to hold string.
constructor(props) {
....
this.state = {
myProperty: 1.42,
myPropertyString: '1.42'
}
}
Set your input value to String one
<input type="text"
onChange={this.handleUpdate}
value={this.state.myPropertyString}/>
In handleUpdate method update both state variables.
handleUpdate(e) {
this.setState({
myProperty: parseFloat(e.target.value),
myPropertyString: e.target.value
});
}

Categories

Resources