Switching on Union types in Flow - react-native

This code uses field.type which is a string to determine what component to render.
The SelectOptionsOverlay component will render a CardFormSelectField (type=="select") with its options.
CardFormTextAreaOverlay will render a CardFormTextAreaField (type=="textarea"), and this component will never try to use options or any other property not present in CardFormTextAreaField.
export type CardFormSelectField = {
title: string,
type: string,
placeholder: string,
options: string[]
};
export type CardFormTextAreaField = {
title: string,
type: string
};
export type CardFormField = CardFormSelectField | CardFormTextAreaField;
overlay(field: CardFormField): ?(SelectOptionsOverlay | CardFormTextAreaOverlay) {
const props = {...};
switch (field.type) {
case "select":
return <SelectOptionsOverlay {...props} />;
case "textarea":
return <CardFormTextAreaOverlay {...props} />;
}
}
The switch statement keeps this all safe. However, Flow doesn't "know about" the switch statement and its safety benefits, so it complains:
Cannot return <SelectOptionsOverlay /> because:
Either React.Element [1] is incompatible with SelectOptionsOverlay [2].
Or React.Element [1] is incompatible with CardFormTextAreaOverlay [2].
[1] is the return statement and [2] is the type declaration for the function's return value: ?(SelectOptionsOverlay | CardFormTextAreaOverlay)
There is also a corresponding error on the second return statement (for CardFormTextAreaOverlay).
I'm actually having trouble understanding what this means, let alone if it's possible to fix it.
On the other hand, which I understand much better, if I remove the type annotation for the return statement, Flow only complains on the return statement with SelectOptionsOverlay, complaining about how the field prop may contain options and placeholder which is not present in CardFormTextAreaField.
That's true, but the switch statement should protect us from having bad props in our actual return. But you can't switch on FlowType can you? Since Flow isn't actual JS?

If it's possible and makes sense for your code, you can specialize the type property for each CardFormField. For example, instead of type: string for CardFormSelectField, you could have type: "select". This way, Flow knows that when it enters the case "select" block, props must be of the type CardFormSelectField. Currently, there is no way for Flow to associate each case statement with a specific type in the union type.
import React from 'react';
type CardFormSelectField = {|
title: string,
type: "select", // NEW!
placeholder: string,
options: string[]
|};
type CardFormTextAreaField = {|
title: string,
type: "textarea", // NEW!
|};
type CardFormField = CardFormSelectField | CardFormTextAreaField;
function SelectOptionsOverlay(props: CardFormSelectField) {
return "SelectOptionsOverlay";
}
function CardFormTextAreaOverlay(props: CardFormTextAreaField) {
return "CardFormTextAreaOverlay";
}
function Overlay(props: CardFormField) {
switch (props.type) {
case "select":
(props: CardFormSelectField);
return <SelectOptionsOverlay {...props} />;
case "textarea":
(props: CardFormTextAreaField);
return <CardFormTextAreaOverlay {...props} />;
}
}
Try Flow

Related

Event only firing as inline JS statement

I have the following code in a Nuxtjs app in SSR mode.
<Component
:is="author.linkUrl ? 'a' : 'div'"
v-bind="!author.linkUrl && { href: author.linkUrl, target: '_blank' }"
#click="author.linkUrl ? handleAnalytics() : null"
>
The click event in case it's an a tag, will only fire if it's written as handleAnalytics(), but handleAnalytics will not work.
Don't get me wrong the code is working, but I don't understand why.
With classical event binding (#click="handleAnalytics), Vue will auto bind it for you because it sees it's a function.
But when provided a ternary condition, it's not auto binded but wrapped into a anonymous function instead. So you have to call it with parenthesis otherwise you're just returning the function without executing it.
To be clearer, you can write it this way: #click="() => author.linkUrl ? handleAnalytics() : null"
Note: when having a dynamic tag component, I'd suggest to use the render function instead.
This is an advanced technique, but this way you won't bind things to an element that doesn't need it (without having the kind of hack to return null).
Example:
export default {
props: {
author: { type: Object, required: true },
},
render (h: CreateElement) {
const renderLink = () => {
return h('a', {
attrs: {
href: author.linkUrl,
target: '_blank',
},
on: {
click: this.handleAnalytics
},
)
}
const renderDiv = () => {
return h('div')
}
return this.author.linkUrl ? renderLink() : renderDiv()
}
}
Documention: Vue2, Vue3
In javascript functions are a reference to an object. Just like in any other language you need to store this reference in memory.
Here are a few examples that might help you understand on why its not working:
function handleAnalytics() { return 'bar' };
const resultFromFunction = handleAnalytics();
const referenceFn = handleAnalytics;
resultFromFunction will have bar as it's value, while referenceFn will have the reference to the function handleAnalytics allowing you to do things like:
if (someCondition) {
referenceFn();
}
A more practical example:
function callEuropeanUnionServers() { ... }
function callAmericanServers() { ... }
// Where would the user like for his data to be stored
const callAPI = user.preferesDataIn === 'europe'
? callEuropeanUnionServers
: callEuropeanUnionServers;
// do some logic
// ...
// In this state you won't care which servers the data is stored.
// You will only care that you need to make a request to store the user data.
callAPI();
In your example what happens is that you are doing:
#click="author.linkUrl ? handleAnalytics() : null"
What happens in pseudo code is:
Check the author has a linkUrl
If yes, then EXECUTE handleAnalytics first and then the result of it pass to handler #click
If not, simply pass null
Why it works when you use handleAnalytics and not handleAnalytics()?
Check the author has a linkUrl
If yes, then pass the REFERENCE handleAnalytics to handler #click
If not, simply pass null
Summary
When using handleAnalytics you are passing a reference to #click. When using handleAnalytics() you are passing the result returned from handleAnalytics to #click handler.

How to update the form state on input type change

I basically have a record with a parameters property that is an array. Each parameter has a type and a value and the value is dependent on the type. E.g. type string means that value can be "foo" or "bar" while type bool means that it has to be true or false.
This is modeled as a SelectInput for the type and a type dependent input for the value (e.g. BooleanInput, TextInput, ..). The inputs are inside a FormDataConsumer, so that the correct value input according to type can be rendered. Something like the following
const renderValueInput = (type, getSource) => {
switch (type) {
case 'bool':
return <BooleanInput source={getSource('value')} label="Value" fullWidth />;
default:
return <TextInput source={getSource('value')} label="Value" fullWidth />;
}
};
return (
<ArrayInput source="parameters">
<SimpleFormIterator>
<FormDataConsumer>
{({ getSource, scopedFormData }) => (
<>
<SelectInput
source={getSource('type')}
label="Type"
choices={[
{ id: 'bool', name: 'Boolean' },
{ id: 'string', name: 'String' },
]}
/>
{ scopedFormData && renderValueInput(scopedFormData.type, getSource) }
</>
)}
</FormDataConsumer>
</SimpleFormIterator>
</ArrayInput>
);
So far this is working fine and rendering the correct components, but if the type is changed apparently the form state is not updated for the value. E.g. if the type is changed from bool to string the value stays true or false in the state. But if I change anything in the value input the state is updated again.
I'm now wondering how I can update the form state for value when changing the type? Any ideas? Or is the model / concept that I chose simply wrong?
I'm also not sure if this is a react-final-form or react-admin "problem" tbh.

How to solve toUpperCase issue in Jest testing?

I am using Vuejs and Jest. I have following statement in my component:
...mapGetters('type', [
'selectedAddressType',
'checkDeliveryType',
]),
const keyToDisplayMessage = `${this.checkDeliveryType.toUpperCase()}_${this.selectedAddressType.toUpperCase()}`;
and in test file, I have following code:
test('If a check is requested', () => {
const selectedCheckAddress = {
addressLine1: 'test',
city: 'test',
state: 'test',
zipCode: '12345',
};
getters.selectedAddressType.mockReturnValue('Home');
getters.checkDeliveryType.mockRejectedValue('Regular');
expect(wrapper.vm.pageTitle).toStrictEqual('Request Submitted');
expect(wrapper.html()).toMatchSnapshot();
});
it is giving an error as
TypeError: this.checkDeliveryType.toUpperCase is not a function
How can we resolve this?
.toUpperCase() is a method provided by the String-class. It seems your variable this.checkDeliveryType isn't a string, hence you can not call this method (as it does not exist on whatever type your variable is at that point in time).
Either fix the type or cast the value manually to a string before and call .toUpperCase() on it afterwards. One way would be:
const checkDeliveryTypeStr = `${this.checkDeliveryType}`;
const keyToDisplayMessage = `${checkDeliveryTypeStr.toUpperCase()}_${this.selectedAddressType.toUpperCase()}`;
But in general it would be a better idea to fix the type correctly in your entire flow.

Is there an easier way of updating nested arrays in react-native (react-redux)?

I am trying to update an array inside of my array (nested array) with react-redux. I found a solution to how to do this but is there any easier way of doing this rather than passing multiple parameter to the action.
[types.HISTORY_UPDATE](state, action){
return {
...state,
budgets: [
...state.budgets.slice(0,action.id),
{
key: action.key,
id: action.idd,
name: action.name,
budgetType: action.budgetType,
startDate: action.startDate,
currency: action.currency,
amount: action.amount,
amountLeft: action.amountLeft,
rollOver: action.rollOver,
color: action.color,
iconName: action.iconName,
history: [
...state.budgets[action.id].history.slice(0,action.histId),
{
note: action.note,
amount: action.amount,
type: action.type,
date: action.date,
fullDate: action.fullDate,
hours: action.hours,
min: action.min,
month: action.month,
year: action.year
},
...state.budgets[action.id].history.slice(action.histId+1)
]
},
...state.budgets.slice(action.id+1)
]
}
},
and the action goes like this
export function updateHistory(id,key,idd,name,budgetType,startDate,currency,amount,amountLeft,rollOver,color,iconName,histId,........){
I don't want to spend time with passing multiple parameter like this while using react-redux and also while I tried to run my application on my phone sometimes it really slows the application. Is it because of the example above?
I would be really appreciated If you guys come up with a solution.
I typically do not store arrays in redux, since updating a single element really is a burden as you noticed. If the objects you have inside your array all have a unique id, you can easily convert that array to an object of objects. As key for each object you take that id.
const convertToObject = (array) => {
let items = {};
array.map((item) => {
items[item.id] = item;
});
return items;
};
In your action you simply just pass the item you want to update as payload, and you can update the redux store very easily. In this example below I am just updating the budgets object, but the same logic applies when you assign a history object to each budget.
[types.BUDGET_UPDATE](state, action){
const item = action.payload;
return {
...state,
budgets: {
...state.budgets,
[item.id]: item
}
}
}
And if you want an array somewhere in your component code, you just convert the redux store object back to an array:
const array = Object.values(someReduxStoreObject);

Sharing data from a VueJS component

I have a VueJS address lookup component.
Vue.component('address-lookup',
{
template: '#address-lookup-template',
data: function()
{
return {
address: {'name': '', 'town:': '', 'postcode': ''},
errors: {'name': false, 'town': false, 'postcode': false},
states: {'busy': false, 'found': false},
result: {}
}
},
methods:
{
findAddress: function(event)
{
if( typeof event === 'object' && typeof event.target === 'object' )
{
event.target.blur();
}
$.ajax(
{
context: this,
url: '/lookup',
data:
{
'name': this.address.name,
'town': this.address.town,
'postcode': this.address.postcode
},
success: function(data)
{
this.states.busy = false;
this.states.found = true;
this.address.name = data.name;
this.result = data;
}
});
},
reset: function()
{
this.states.found = false;
this.result = {};
}
}
});
Inside my template I've then bound the result like so:
<p>{{ result.formatted_address }}</p>
There is some extra data returned within the result (like a twitter handle) that isn't part of the address lookup template, and occurs on a separate part of the form. For reasons relating to how my form is structured I can't include these inputs within the same template.
I found a way to bind those inputs, although it felt somewhat 'hacky'.
<input type="text" name="twitter" v-model="$refs.lookupResult._data.result.twitter">
That all works fine.
My problem is that the form is included as part of a larger template sometimes in the context of creating a new record, sometimes in the context of editing. When editing a record, the lookup component is removed (using an if server-side, so the template is no longer loaded at all) and when that happens I get this error.
$refs.lookupResult._data.result.twitter": TypeError: Cannot read property '_data' of undefined
This makes sense. lookupResult is defined when I include the template, and when editing I am removing this line:
<address-lookup v-ref:lookup-result></address-lookup>
I've worked around it by including a version of each extra input without the v-model attribute, again using a server-side if. But there are quite a few of these and it's getting a bit messy.
Is there a cleaner approach I could be using to better achieve this?
So I don't know the hierarchy of your layout, it isn't indicated above, but assuming that address-lookup component is a child of your parent, and you in fact need the results of address lookup in that parent, eg:
<parent-component> <!-- where you need the data -->
<address-lookup></address-lookup> <!-- where you lookup the data -->
</parent-component>
then you can simply pass the data props, either top-down only (default) or bidirectionally by defining 'address' for example on your parent's vue data hook:
// parent's data() function
data = function () {
return {
address: {}
}
}
// parent template, passed address with .sync modifier (to make it bi-directional)
<parent-component>
<address-lookup :address.sync='address'></address-lookup>
</parent-component>
// have the props accepted in the address look up component
var addressComponent = Vue.extend({
props: ['address']
})
Now in your $.ajax success function, simply set the props you need on this.address. Of course you can do this with all the props you need: errors, results, state etc. Even better, if you can nest them into a single key on the parent, you can pass the single key for the object containing all four elements instead of all four separately.