Vue v-model on input as a dynamic ref - html

I created this <input> Vue component that handles a v-model property.
Adding the two-way data binding wasn't a problem, but I now want to add the current input value to the "state" so that I can clear it by just modifying a ref.
clearValue is called as expected but updating inputValue doesn't update the UI. What should I be doing differently ?
<template>
<div>
<input
:id="id"
type="text"
:value="inputValue"
#input="updateValue"
/>
<UiIcon
type="x"
#click.native="clearValue"
/>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from '#nuxtjs/composition-api';
import UiIcon from '~/components/ui/UiIcon.vue';
export default defineComponent({
name: 'UiInput',
components: { UiIcon },
props: {
id: {
type: String,
required: true,
},
value: {
type: String,
default: undefined,
},
},
setup(props, { emit }) {
const inputValue = ref(props.value);
const clearValue = () => {
inputValue.value = '';
};
const updateValue = (event: Event) => {
emit('input', (event.target as HTMLInputElement).value);
};
return {
updateValue,
clearValue,
inputValue,
};
},
});
Usage example
data() {
return { text: '' };
},
template: `
<div>
<UiInput :id="id" v-model="text" />
</div>
`,

I think you just need to emit 'input' event with empty value inside clearValue function
emit('input', '');

Related

Why useReducer does not update state in react-typescript

I have a problem with Typescript with React and useReducer hook. The state does not wanted to be updated and it is 100% typescript problem (I mean I need a typescript solution because in javascript it works perfect). So I want to make reducer as short as possible so I use "name" in HTML and get this name as a name of object in initialState. When I return ( title:{}, description: {}) It works but when I use [action.field] it does not work. action.field is the name of HTML element.
const initialStateReducer: inputsFormState = {
title: {
val: "",
isValid: false,
},
description: {
val: "",
isValid: false,
},
};
const RecipeForm = () => {
const inputReducer = (
state: inputsFormState,
action: inputsFormAction
): inputsFormState => {
console.log(action.type, action.content, action.field);
let isValid: boolean;
const { content } = action;
isValid = content.length > 0;
return {
[action.field]: {
val: content,
isValid: isValid,
},
...state,
};
};
const [formIsValid, setFormIsValid] = useState<boolean>(false);
const [inputsValues, dispatchReducer] = useReducer(
inputReducer,
initialStateReducer
);
const changeTextHandler = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
dispatchReducer({
type: ActionKind.changeVal,
field: e.target.name,
content: e.target.value,
});
};
return (
<React.Fragment>
<Input
name="title"
onChange={(e) => changeTextHandler(e)}
placeholder="Name for your recipe"
/>
<Textarea
name="description"
onChange={(e) => changeTextHandler(e)}
placeholder="Description"
cols={20}
rows={20}
resize="none"
/>
</React.Fragment>
);
};
Typescript is only a superset of JS that adds on Typing while writing code, it has no effect on the actually running of the JS (as it gets compiled into JS before running).
From looking at the above code I think the issue is your return in the reducer:
return {
[action.field]: {
val: content,
isValid: isValid,
},
...state,
};
Should be:
return {
...state,
[action.field]: {
val: content,
isValid: isValid,
}
};
As (and this may not be a great explanation) but right most arguments overwrite the preceding values, so you're effectively overwriting the new with the original.

next-i18next i18n on array of objects

I have an array of objects called "Options", that I use as a prop to a dropdown/select Material-UI component. I want to use the next-i18next library on the labels. I already implemented with success through all the next app just like the documentation explains. I tried using the {t('key')} and it doesn't allow.
import { useTranslation } from 'next-i18next'
const UsersPage = () => {
const { t } = useTranslation('user');
const Options = [
{ value: 'fullName', label: 'Nome' },
{ value: 'cpf', label: 'CPF' },
{ value: 'id', label: 'PadrĂ£o' },
]
...rest of the component
}
export const getStaticProps = async ({ locale }) => ({
props: {
...await serverSideTranslations(locale, ['user', 'home']),
},
})
export default UsersPage;
The msefer answer is right:
`${t("key")}`
inside JSON or string building in props like
const since = `${t('since')}`;
const until = `${t('until')}`;
...
<ListItemText
primary={value.name}
secondary={since + value.beginDate + until + value.endDate}
/>

Antd customRender return raw html

I need to generate html code in a customRender function of one column.
I cannot use scopedSlots as suggested here, since the html code is part of e generic component, and other components pass their columns array as a parameter.
BaseComponent.vue:
<template>
<a-table
:columns="attrs.columns"
:rowKey="record => record[attrs.recordId]"
:dataSource="filteredTableData"
>
</a-table>
</template>
<script>
export default {
props: {
attrs: {
type: Object,
required: true
}
:
</script>
ContactComponent.vue:
<template>
:
<base-component :attrs="attrs"/>
:
</template>
<script>
import BaseComponent from './BaseComponent';
export default {
components: {
BaseComponent
},
data() {
return {
attrs: {
columns: [
title: 'Type',
dataIndex: 'type',
customRender: (val, record) => {
return '<div class="myClass">' + val + </div>';
},
],
recordId: 'contactId'
}
}
}
:
</script>
The problem:
The following code:
customRender: (val, record) => {
return '<div class="myClass">' + val + '</div>';
},
renders this:
Is there a way to force raw html rendering directly from the customRender function?
You can transform your code:
customRender: (val, record) => {
return '<div class="myClass">' + val + '</div>';
},
In this way (if you have JSX support).
customRender: (data) => {
return <div class="myClass"> {data.text} </div>;
},
If you dont have JSX support, you can return a Vnode. Like specified here: https://vuejs.org/guide/extras/render-function.html#creating-vnodes (I didn't try this way).
Or you can try to add support for JSX: https://vuejs.org/guide/extras/render-function.html#jsx-tsx
(My reply is late but may help others.)
==========
Edit:
Here, another exemple, to show you where this piece of code should be (only work with JSX support):
data: function () {
return {
dataSource: [],
columns: [
{
title: 'Website',
dataIndex: "cat_website",
key: "cat_website",
customRender: (data) => {
return <a href={'http://' + data.text} target='_blank'>{data.text}</a>;
},
},
// other columns...
],
// ...
}
}

React: Passing a Prop in w/ event for setState

I'm working with a nested state object that I have been updating with onChange functions, like so:
const [someState, setSomeState] = useState({
customer: [
{
name: "Bob",
address: "1234 Main Street",
email: "bob#mail.com",
phone: [
{
mobile: "555-5555",
home: "555-5555"
}
]
}
]
});
const updateSomeStatePhone = e => {
e.persist();
setSomeState(prevState => {
prevState.customer[0].phone[0].mobile = e.target.value;
return {
...prevState
};
});
};
<p>Update Mobile Number<p>
<select
value={someState.customer[0].phone[0].mobile}
onChange={updateSomeStatePhone}
>
<option value="123-4567">"123-4567"</option>
</select>
This gets the trick done. Currently however, if I want to update multiple state properties via a large form with dropdowns/input fields etc, I have to hard code 6 different onChange handlers for those fields.
Instead, I would prefer to have only one onChange handler, and pass in the state from the form field for the state property that I am changing, but I can't figure out the syntax:
const updateSomeState = (e, prop) => {
e.persist();
setSomeState(prevState => {
prevState.prop = e.target.value;
return {
...prevState
};
});
};
<p>Update Mobile Number<p>
<select
value={someState.customer[0].phone[0].mobile}
onChange={updateSomeState(e, prop)}
>
<option value="123-4567">"123-4567"</option>
</select>
I've tried using different types of syntax to chain the passed in 'prop' value to prevState:
prevState.prop = e.target.value;
prevState.(prop) = e.target.value;
${prevState} + '.' + ${prop} = e.target.value; // Dumb, I know
But the function never recognizes the "prop" that I pass in from the function. I'm sure there must be a simple way to do this. Any help would be greatly appreciated.
Does it have to be a single useState hook? I would recommend using useReducer or simplifying it a bit with multiple useState hooks.
Multiple useState hooks
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [name, setName] = React.useState("");
const [address, setAddress] = React.useState("");
const [email, setEmail] = React.useState("");
const [mobile, setMobile] = React.useState("");
const [home, setHome] = React.useState("");
const getResult = () => ({
customer: [
{
name,
address,
email,
phone: [
{
mobile,
home
}
]
}
]
});
// Do whatever you need to do with this
console.log(getResult());
return (
<>
<input
value={name}
placeholder="name"
onChange={e => setName(e.target.value)}
/>
<br />
<input
value={address}
placeholder="address"
onChange={e => setAddress(e.target.value)}
/>
<br />
<input
value={email}
placeholder="email"
onChange={e => setEmail(e.target.value)}
/>
<br />
<input
value={mobile}
placeholder="mobile"
onChange={e => setMobile(e.target.value)}
/>
<br />
<input
value={home}
placeholder="home"
onChange={e => setHome(e.target.value)}
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Single useReducer (with simplified state)
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const reducer = (state, action) => {
const { type, value } = action;
switch (type) {
case "SET_NAME":
return { ...state, name: value };
case "SET_ADDRESS":
return { ...state, address: value };
case "SET_EMAIL":
return { ...state, email: value };
case "SET_MOBILE":
return { ...state, phone: [{ ...state.phone[0], mobile: value }] };
case "SET_HOME":
return { ...state, phone: [{ ...state.phone[0], home: value }] };
default:
throw Error(`Unexpected action: ${action.type}`);
}
};
const initialState = {
name: "",
address: "",
email: "",
phone: [
{
mobile: "",
home: ""
}
]
};
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
// Do what you need with state
console.log(state);
return (
<>
<input
value={state.name}
placeholder="name"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_NAME", value })
}
/>
<br />
<input
value={state.address}
placeholder="address"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_ADDRESS", value })
}
/>
<br />
<input
value={state.email}
placeholder="email"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_EMAIL", value })
}
/>
<br />
<input
value={state.phone.mobile}
placeholder="mobile"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_MOBILE", value })
}
/>
<br />
<input
value={state.phone.home}
placeholder="home"
onChange={({ target: { value } }) =>
dispatch({ type: "SET_HOME", value })
}
/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
useReducer is a better choice for doing this. Examples all over the internet.
Why you shouldn't use useState to pass an object is because it doesn't act like setState. The underlying object reference is the same. Therefore, react will never trigger a state change. In case you want to use the same useState for objects. You may have to implement your own version to extend that (example below ) or you can directly use useReducer hook to achieve the same.
Here's an example with useState for you to notice the state update on every change.
const [form, setValues] = useState({
username: "",
password: ""
});
const updateField = e => {
setValues({
...form,
[e.target.name]: e.target.value
});
};
Notice the ...form in there. You can do it this in every update you want or you can use your own utility or useReducer as I mentioned.
Now coming to your code, there are other concerns.
You are using your phone as an array which can be an object. Or better yet separate properties will do as well. No harm.
If you have customers as an array, you have to loop through the records. Not just update the index by hardcoding. If there's only one customer better not keep the array but just an object. Assuming it is an array of customers, and you are looping through it, here's how to update mobile.
const updatedCustomers = state.customers.map(item => {
const { phone } = item;
return { ...item, phone: { mobile: e.target.value }};
// returns newCustomer object with updated mobile property
});
// Then go ahead and call `setSomeState ` from `useState`
setSomeState(...someState, { customer: updatedCustomers });// newState in your case is
Instead, I would prefer to have only one onChange handler, and pass in
the state from the form field for the state property that I am
changing, but I can't figure out the syntax
If you haven't figured that out from the first example. Here's how in short steps.
Give your HTML element a name attribute.
Then instead use the [e.target.name]
return { ...item, phone: { [e.target.name]: e.target.value }};
Use lodash's _.set helper.
const updateSomeState = (e, prop) => {
e.persist();
setSomeState(prevState => {
let customers = [...prevState.customers] // make a copy of array
let customer = {...customers[0]} // make a copy of customer object
_.set(customer, prop, e.target.value)
customers[0] = customer;
return {
...prevState, customers
};
});
};
BTW, in your existing updateSomeStatePhone you are modifying prevState object which is supposed to be immutable.

Vuejs component does not render immediately

I have a vue app and a component. The app simply takes input and changes a name displayed below, and when someone changes the name, the previous name is saved in an array. I have a custom component to display the different list items. However, the component list items do not render immediately. Instead, the component otems render as soon as I type a letter into the input. What gives? Why would this not render the list items immediately?
(function(){
var app = new Vue({
el: '#app',
components: ['name-list-item'],
data: {
input: '',
person: undefined,
previousNames: ['Ian Smith', 'Adam Smith', 'Felicia Jones']
},
computed: {
politePerson: function(){
if(!this.person) {
return 'Input name here';
}
return "Hello! To The Venerable " + this.person +", Esq."
}
},
methods: {
saveInput: function(event){
event.preventDefault();
if(this.person && this.previousNames.indexOf(this.person) === -1) {
this.previousNames.push(this.person);
}
this.setPerson(this.input);
this.clearInput();
},
returnKey: function(key) {
return (key + 1) + ". ";
},
clearInput: function() {
this.input = '';
},
setPerson: function(person) {
this.person = person;
}
}
});
Vue.component('name-list-item', {
props: ['theKey', 'theValue'],
template: '<span>{{theKey}} {{theValue}}</span>'
});
})()
And here is my HTML.
<div id="app">
<div class="panel-one">
<span>Enter your name:</span>
<form v-on:submit="saveInput">
<input v-model="input"/>
<button #click="saveInput">Save</button>
</form>
<h1>{{politePerson}}</h1>
</div>
<div class="panel-two">
<h3>Previous Names</h3>
<div>
<div v-for="person, key in previousNames" #click='setPerson(person)'><name-list-item v-bind:the-key="key" v-bind:the-value="person" /></div>
</div>
</div>
</div>
You are not defining your component until after you have instantiated your Vue, so it doesn't apply the component until the first update.