I have a piece of code, using cycle.js and the reactive xstream library, like the one shown below. It consists of a form field whose value is rendered in a p tag on submit. The questions follow:
1. How can the form's input field be reset to the default value when the form is submitted?
2. Is there a way to reset the input$ and submit$ streams to their initial values after submit?
function formIntent (sourcesDOM) {
const fNameInput$ = sourcesDOM
.select('form#user .fname')
.events('input')
.compose(debounce(1000))
.map((e: {target: any}) => e.target.value)
const submit$ = sourcesDOM
.select('form#user')
.events('submit')
.map((e) => e.preventDefault())
.mapTo({type: 'USER'})
const actions$ = xs.combine(fNameInput$, submit$)
const initState = {fNameInput: ''}
return actions$
.map(([fNameInput, {type}]) => {
return type === 'USER' ? {fNameInput} : initState
})
.startWith(initState)
}
function formView (state$) {
return state$
.map(({fNameInput}) =>
div([
form('#user', [
label('Name'),
hr(),
input('.fname', {attrs: {id: 'first-name', type: 'text', placeholder: 'First name'}}),
hr(),
input('.sub', {attrs: {type: 'submit', value: 'Save'}})
]),
div([
h2('Submitted value'),
p(fNameInput),
hr()
])
])
)
}
Is there a better way to create a form like this?
P.S. the output of formIntent function is fed into the input of the formView function.
This is what I ended up doing. It does what I wanted. I will still be interested to know a better way of achieving the same goal.
function formIntent (sourcesDOM) {
const fNameInput$ = sourcesDOM
.select('form#user .fname')
.events('input')
.compose(debounce(1000))
.filter((e: {target: any}) => e.target.value.length > 2)
.map((e: {target: any}) => e.target)
const submit$ = sourcesDOM
.select('form#user')
.events('submit')
.map(e => {
e.preventDefault()
console.log('3: Inside intent', e)
return {user: 'CLICKED'}
})
return xs.combine(fNameInput$, submit$)
}
function formModel (actions$) {
const initState = {user: '', fNameInput: {}}
return actions$
.map(([fNameInput, user]) => {
const fName = fNameInput.value
if (user.user === 'CLICKED') {
fNameInput.value = fNameInput.defaultValue
user.user = 'DISPLAY'
return {user: user.user, fNameInput: fName}
} else return initState
})
.startWith(initState)
}
function formView (state$) {
return state$
.map(({user, fNameInput}) =>
div([
form('#user', [
label('Name'),
hr(),
input('.fname', {attrs: {id: 'first-name', type: 'text', placeholder: 'First name'}}),
hr(),
input('.sub', {attrs: {type: 'submit', value: 'Save'}})
]),
(user === 'DISPLAY') && div([
h2('Submitted value'),
p(fNameInput),
hr()
])
])
)
}
function main (sources) {
const actions$ = formIntent(sources.DOM)
const state$ = formModel(actions$)
const vtree$ = formView(state$)
return {
DOM: vtree$
}
}
You can use hook for updating input value when DOM refreshed. So if you use #cycle/dom version >11.0 which uses Snabbdom you can use as this:
input({
hook: {
update: (o, n) => n.elm.value = ''
}
})
Where empty string you can pass default value which you want. It will refresh input value every time when state updates.
Related
This is created another objects dynamically when you click a button. But I want to create new object inside questions Array when I click a button.
const [inputFields, setInputFields] = useState([
{
sectionName: "",
sectionDesc: "",
questions: [{ questionType: "", questionText: "" }],
},
]);
const handleChange = (index: any, event: any) => {
const values = [...inputFields];
// #ts-ignore
values[index][event.target.name] = event.target.value;
setInputFields(values);
console.log(index, event.target.name);
};
const handleAddFields = () => {
setInputFields([
...inputFields,
{
sectionName: "",
sectionDesc: "",
questions: [{ questionType: "", questionText: "" }],
},
]);
};
First off, you are mutating state in your handleChange handler. You can avoid this using map()
const handleChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
setInputFields(prev => prev.map((p, i) => (
i === index
? {
...p,
[event.target.name]: event.target.value
}
: p
)));
};
As for adding to the questions array, you will need an index for which object you want to add to, then use the same technique as above to spread into that object.
const handleAddFields = (index: number) => {
setInputFields(prev => prev.map((p, i) => (
i === index
? {
...p,
questions: [...p.questions, { questionType: "", questionText: "" }]
}
: p)
));
};
Note: Using any for all parameters defeats the purpose of typing, try to use relevant types to take advantage of the type system.
I have been stumped by this looking for some direction. I have a state object:
const state = [
{type: 'Primary', number: 123456},
{type: 'Mobile', number: 789012}
]
I have a function which is giving me the updated values for the oldValue, newValue, and index.
How would I be able to replace the specific number value for example in the 'Mobile' object and return the new array?
If you have the state array index you need to change, the newValueand oldValue:
const newState = state.map((obj, i) => {
if(i === index && obj.number === oldValue) {
let newObj = { ...obj };
newObj.number = newValue;
return newObj;
}
return obj;
}
You can use array.find() to find the corresponding object and replace a specific value :
const state = [
{type: 'Primary', number: 123456},
{type: 'Mobile', number: 789012}
]
// This will find and return the FIRST item that matches, or undefined if none are found
const ObjectToChange = state.find(elem => elem.type === 'Mobile')
if (ObjectToChange != undefined)
ObjectToChange.number = 42;
console.log(state);
If you mean change the value:
const state = [
{type: 'Primary', number: 123456},
{type: 'Mobile', number: 789012}
];
state[state.findIndex(item=>item.type==='Mobile')]={type:'Mobile',number:1111}
console.log(JSON.stringify(state,null,2));
const state = [
{type: 'Primary', number: 123456},
{type: 'Mobile', number: 789012}
]
const someFunction = (state, oldValue, newValue, index) => {
let newState = state.map(e => {
if(e.type === index) e.number = newValue;
return e
})
return newState
}
const newState = someFunction(state, '', 123456, 'Mobile')
console.log(newState)
You could use Array.map to return the updated array like so:
const updateFunction = (state, oldValue, newValue, index) => {
return state.map((stateObject, i) => i === index ? ({ ...stateObj, number: newValue }) : stateObject);
}
After an api call fetches data I need to call a number of further actions on FETCH_DATA_SUCCESS.
The 'RESET_IMAGE_DATA' and 'INITIALISE_FILTERS' actions must be called on every 'FETCH_DATA_SUCCESS'. However, 'SET_PIVOT' must be called only when action.context === 'pivot'.
So, there are 2 possible cases.
In the first case, 'RESET_IMAGE_DATA' and 'INITIALISE_FILTERS' are called.
In the second case, 'RESET_IMAGE_DATA', 'INITIALISE_FILTERS' and 'SET_PIVOT' are called.
I've tried all kinds of solutions with no success and my latest attempt is below. Any help would be appreciated.
const loadDataEpic = (action$, state$) =>
action$.pipe(
ofType('FETCH_DATA_SUCCESS'),
mergeMap(action => {
if (action.context === 'pivot') {
return of({
type: 'SET_PIVOT',
});
}
return of(
{
type: 'RESET_IMAGE_DATA',
},
{
type: 'INITIALISE_FILTERS',
}
)}
)
);
You can change of to from so you send an array, since an array allows easy dynamic insertion.
Like this:
const loadDataEpic = (action$, state$) =>
action$.pipe(
ofType('FETCH_DATA_SUCCESS'),
mergeMap(action => {
const pivotActions = action.context === 'pivot'
? [{ type: 'SET_PIVOT' }]
: [];
return from([
...pivotActions,
{
type: 'RESET_IMAGE_DATA'
},
{
type: 'INITIALISE_FILTERS'
}
]);
})
);
try this:
const loadDataEpic = (action$, state$) =>
action$.pipe(
ofType('FETCH_DATA_SUCCESS'),
mergeMap(action => {
const isPivot = action.context === 'pivot';
return of(
{ type: 'RESET_IMAGE_DATA' },
{ type: 'INITIALISE_FILTERS' },
...(isPivot ? [{ type: 'SET_PIVOT' }] : [])
);
})
);
it will only add the action SET_PIVOT if the condition is true.
p.s.: If you don't like the syntax you could use an array and push it based on the condition and then return it with of(...actions)
I've got these Interfaces:
export interface QueryObject {
id: ID;
path: string[];
filters: Filters;
}
export interface Filters {
state: string;
localeID: string;
role: string;
}
And trying to come up with a Functional Programming solution in order to just copy the properties that exist and have a value from a tmp object to the existing data model. Now,.. obviously this doesnt work. Filters will be completely overwritten while losing the properties localeID and role.
let queryObject: QueryObject;
let filters: Filters = { state: 'GB'}; // this obviously gives an error on Interface implementation
queryObject.filters = filters;
Right now I'am taking the original object, traversing to the property and overwriting it with the updated value.
const queryObject: QueryObject = _.cloneDeep(this.queryObject);
queryObject.filters.state = state; // 'GB'
this.portareService.update(queryObject, this.portareQuery.getActiveId());
Would be nice to solve this with a Object.assign or spread ... solution like:
{
return ...createQueryObject, updatedQueryObject
}
I know how to do this with a function using loop(s), but looking for a Functional Programming approach to this.
You could implement a concat method for both QueryObject and Filters. In the concat, you define what "merge logic" you want to use. The QueryObject calls the Filters' concat method internally.
Within the concat methods, you can use the spread syntax or any other logic to ensure new objects are created and you're not mutating anything.
By adding an empty constructor, you can easily start using those concatenators inside a reduce or other automated merge.
I found this blog post on Semigroups by Tom Harding super inspiring. This post about Monoids has some info about the empty part in it.
const QueryObject = ({id = null, path = null, filters = Filters.empty() })=> ({
id,
path,
filters,
concat: other => QueryObject({
id: other.id || id,
path: other.path || path,
filters: filters.concat(other.filters)
}),
toString: () => `QueryObject(${id}, ${path}, ${filters.toString()})`
});
QueryObject.empty = () => QueryObject({});
QueryObject.merge = (x, y) => x.concat(y);
const Filters = ({ state = null, localeID = null, role = null }) => ({
state,
localeID,
role,
concat: other => Filters({
state: other.state || state,
localeID: other.localeID || localeID,
role: other.role || role
}),
toString: () => `Filters(${state}, ${localeID}, ${role})`
});
Filters.empty = () => Filters({});
Filters.merge = (x, y) => x.concat(y);
const userFilter = Filters({ role: "User" });
const gbFilter = Filters({ localeID: "GB" });
const filterSettings = [userFilter, gbFilter];
const mergedFilter = filterSettings.reduce(Filters.merge, Filters.empty());
console.log(
"Merged Filter:",
mergedFilter.toString()
);
// Some base query
const accountQuery = QueryObject({ id: "CUSTOM_Q_1", path: "/accounts" });
// Derived queries
const userQuery = accountQuery.concat(QueryObject({ filters: userFilter }));
const gbQuery = accountQuery.concat(QueryObject({ filters: gbFilter }));
console.log(
"User Query:",
userQuery.toString()
);
console.log(
"Brittish Users Query",
userQuery.concat(gbQuery).toString()
);
Edit:
Of course, without the "theory", there's also the more generic:
const uniques = xs => Array.from(new Set(xs));
const nullMergeStrategy = (obj1, obj2) =>
uniques(
Object.keys(obj1)
.concat(Object.keys(obj2))
).reduce(
(acc, k) => Object.assign(acc, { [k]: obj2[k] || obj1[k] }),
{}
);
const Filter = ({ state = null, localeID = null, role = null }) =>
({ state, localeID, role });
const userFilter = Filter({ role: "User" });
const gbFilter = Filter({ localeID: "GB" });
console.log(
nullMergeStrategy(userFilter, gbFilter)
)
I would like to watch over an object, so all the subscribers will be informed for any changes of it.
I saw it already been asked before,
yet the answer is irrelevant since RXjs verion 5 do not include the ofObjectChanges in it's API anymore.
I've looked at some "hacks" like creating an observer which return a function:
let myObservable = new Observable((observer) => {
return (data) => {
observer.next(data)
}
})
//...
myObservable.subscribe()('someData')
However, I'm sure there is more elegant way of doing it.
Any Ideas?
The ES6 way of observing an object is with Proxies. You create a Proxy that wraps the original object and do your work on it. You can use it to create something similar to Observable.ofObjectChanges. Here a partial implementation (only set. You'd need to implement the other traps):
Observable.ofProxyChanges = (target) => {
let subject = new Subject
let proxy = new Proxy(target, {
set(target, key, val) {
let oldValue = target[key]
target[key] = val
subject.next({
type: oldValue === undefined ? "add" : "change",
object: target,
name: key,
oldValue: oldValue
})
}
})
return [proxy, subject.asObservable()]
}
let [obj, objChange$] = Observable.ofProxyChanges({})
objChange$.subscribe(console.log)
obj.bar = 1 // logs { type: "add", name: "bar", object: { bar: 1 } }
obj.foo = 2 // logs { type: "add", name: "foo", object: { bar: 1, foo: 2 } }
obj.foo = 3 // logs { type: "change", name: "foo", object: { bar: 1, foo: 3 }, oldValue: 2 }
I would suggest using something similar to redux approach, when changes to the object can be made in predefined way:
function factory(reducerByType, initialState) {
const action$ = new Rx.Subject();
const state$ = action$
.startWith(initialState)
.scan((state, action) => {
if (reducerByType.hasOwnProperty(action.type)) {
return reducerByType[action.type](state, action);
}
return state;
})
.distinctUntilChanged();
return {
action$,
state$,
dispatch: action => action$.next(action)
}
}
const {state$, dispatch} = factory({
ADD: (state, action) => state + action.number,
SUBTRACT: (state, action) => state - action.number,
}, 0);
state$.subscribe(val => console.log(val));
dispatch({
type: 'ADD',
number: 10,
});
dispatch({
type: 'SUBTRACT',
number: 15,
});
dispatch({
type: 'SUBTRACT',
number: 0,
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.4.0/Rx.js"></script>
You need to use Behavior Subject . https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/behaviorsubject.md