While learning Angular and NGRX, ran into what I thought would be simple example of common problem but having trouble understanding best way to accomplish the control logic with in the Redux store\effects patterns.
General Process:
User types in credit card info, hit pay button > Dispatch a "GetTokenAction" from component > Make an Http request to external 3rd party API to tokenize > Submit that information if successful to Payment API
Here is my latest attempt:
#Effect() getToken$ = this.actions$
.ofType(TokenizerActions.GET_TOKEN)
.pipe(switchMap((action: TokenizerActions.GetTokenAction) => {
return this.TokenizerApiService.getToken(action.payload)
.pipe(
map(tokenResponse => {
console.log(tokenResponse);
// service will return 200 with "declined" status. In this case I want to treat it like an Error.
if (tokenResponse.results.Error != null) {
return new TokenizerActions.GetTokenFailedAction(tokenResponse.results.Error.messages);
}
// What is best practice here? Call Payment Service? Dispatch Actions? Where should this mapping logic live?
const paymentRequest: PaymentRequest = new PaymentRequest();
paymentRequest.token = tokenResponse.results.token;
paymentRequest.amount = action.payload.amount;
paymentRequest.customerNumber = action.payload.customerNumber;
paymentRequest.correlationId = tokenResponse.results.correlation_id;
// this does not work, "dispatched an invalid action: undefined" error.
mergeMap((payReq: PaymentRequest) => [new paymentActions.MakePaymentAction(paymentRequest),
new TokenizerActions.GetTokenSuccessAction(tokenResponse.results.token)]);
}),
catchError(error => of(new TokenizerActions.GetTokenFailedAction(error)))
);
}));
constructor(
private actions$: Actions,
private TokenizerApiService: TokenizerApiService,
private paymentApiService: PaymentApiService
) { }
Question/Considerations:
Is the effect the appropriate place to handle this? The first working version had the component controlling the flow and dispatching multiple actions, could also be handled within the services, not sure which is best practice.
What is the preferred method for error notification within the Effects pattern? Online and in sample application there are a lot of simple examples, but I am having trouble translating that to a slightly more complex example (inspect response and then throw errors and stop processing as needed). Currently the application is doing something like this:
<span class="error" *ngFor="let error of tokenErrors$ | async">{{error.description}}</span>
<span class="error" *ngFor="let error of paymentErrors$ | async">{{error.description}}</span>
<div class="success" *ngIf="(payment$ | async)?.transactionStatus === 'approved'">Success</div>
this.paymentErrors$ = this.store.select(state => state.payment.paymentErrorMessages);
this.tokenErrors$ = this.store.select(state => state.token.tokenErrorMessages);
this.payment$ = this.store.select(state => state.payment.paymentStatus);
Can it be simpler? Should errors be combined into one PayError array? Is there a catch in the component level if subscribed or is it all to be handled in the effects level?
First, don't do the arrow function directly and create selectors instead.
Secondly, select a ViewModel (transactionInfo) that's needed for this component with one selector instead of having 3 different ones. You can combine multiple selectors to achieve that.
The result could be pushed higher in the template with *ngIf, for example
<ng-container *ngIf="transactionInfo$ | async as transactionInfo">
<span class="error" *ngFor="let error of tokenErrors">{{error.description}}</span>
<span class="error" *ngFor="let error of transactionInfo.paymentErrors">{{error.description}}</span>
<div class="success" *ngIf="transactionInfo.transactionStatus === 'approved'">Success</div>
</ng-container>
Related
I'm working in Nuxt3 and I've got a slightly unusual setup trying to watch or retrieve data from child components in a complex form that is structured as a multi-step wizard. It's obviously Vue underneath and I'm using the composition API.
My setup is that I have a page the wizard component is on, and that component has a prop that is an array of steps in the wizard. Each of these steps is some string fields for titles and labels and then a component type for the content. This way I can reuse existing form blocks in different ways. The key thing to understand is that the array of steps can be any length and contain any type of component.
Ideally, I'd like each child component to be unaware of being in the wizard so it can be reused elsewhere in the app. For example, a form that is one of the steps should handle its own validation and make public its state in a way the wizard component can read or watch.
The image below explains my basic setup.
The page includes this tag:
<Wizard :steps="steps" :object="project" #submit="createProject"/>
The Wizard loops over the steps to create each component.
<div v-for="(step) in steps" :key="step.name">
<component v-if="step.status === 'current'" :is="step.content.component" />
</div>
The data to setup the component with the right props for the wizard itself and the child component props.
const steps = ref([
{
name: 'overview',
title: t('overview'),
subTitle: t('projectCreateOverviewDescription'),
status: 'current',
invalid: true,
content: {
component: Overview,
props: null,
model: {}
}
},
{
name: 'members',
title: t('members'),
subTitle: t('projectCreateMembersDescription'),
status: 'upcoming',
invalid: false,
content: {
component: ThumbnailList,
props: {
objects: users,
title: t('users'),
objectNameSingular: t('user'),
objectNamePlural: t('users'),
So far I've tried to dynamically create references in the wizard component to watch the state of the children but those refs are always null. This concept of a null ref seems to be the accepted answer elsewhere when binding to known child components, but with this dynamic setup, it doesn't seem to be the right route.
interface StepRefs {
[key: string]: any
}
let stepRefs: StepRefs = {}
props.steps.forEach(step => {
stepRefs[step.name] = ref(null)
watch(() => stepRefs[step.name].value, (newValue, oldValue) => {
console.log(newValue)
console.log(oldValue)
}, { deep: true })
})
Can anyone direct me to the right approach to take for this setup? I have a lot of these wizards in different places in the app so a component approach is really attractive, but if it comes to it I'll abandon the idea and move that layer of logic to the pages to avoid the dynamic aspect.
To handle changes in child components I'd recommend to use events. You can have the children emit an event on change or completion, and the wizard is listening to events from all children and handling them respectively.
On the wizard subscribe to the event handler of the step component, and process the data coming from each step on completion (or whatever stage you need).
This way you don't need any special data type for the steps, they can just be an array. Simply use a ref to keep track of the current step. You don't even need a v-for, if you just display one step at a time. For a wizard navigation you might still need a v-for, but it would be much simpler. Please see a rough example below.
<div>
<stepComponent step="currentStep" #step-complete="handleStepComplete"/>
<div>
<wizardNavigationItemComponent v-for="step in steps" :active="step.name === currentStep.name" />
</div>
</div>
<script setup lang="ts">
const steps = step[/*your step data here*/]
const currentStepIndex = ref(0)
const currentStep = ref(steps[currentStepIndex.value])
function handleStepComplete(data) {
/* handle the data and move to next step */
currentStepIndex.value = currentStepIndex.value + 1 % steps.length
}
</script>
In the component you just need to define the event and emit it when the data is ready, to pass along the data:
<script setup lang="ts">
const emit = defineEmits<{
(event: "stepComplete", data: <your data type>): void
}>()
/* call emit in the component when its done / filled */
emit("stepComplete", data)
</script>
I hope this helps and can provide a viable path forward for you!
In an Ionic Project, I have:
import { AngularFireDatabase, FirebaseListObservable} from 'angularfire2/database';
and a class with the field:
songs: FirebaseListObservable<any>;
therefore, the line of code:
this.songs = db.list('/songs');
works and allows me to put the line:
<button ion-button ouline *ngFor="let song of songs | async">
in my html without problem.
Now, FirebaseListObservable extends the RxJS Observable (source).
Furthermore, Observable has a method toArray(). But, when I run my project, I see:
Typescript Error
Property 'toArray' does not exist on type 'FirebaseListObservable<any>'.
Why is this? Is there another way I can get an array from what songs is observing?
I don't really sure why the toArray() not working , but i can suggest you a way to get the array you want from the DB.( I usually do that when i want just the array without the ability to listen to any changes of the DB - like Observable does) :
this.db.list('/songs')
.first().toPromise()
.then(response => {
//code to handle the response - in this case its a list
This.items = response;
})
.catch(error => { //error code here });
dont forget to import the rxjs first and toPromise
I really hope it fits your wish and helps you :)
I have a FirebaseListObservable and want to iterate over the resulting elements to create ion-slides:
<ion-slides [initialSlide]="currentDay - 1">
<ion-slide *ngFor="let secret of secrets | async let i = index;">
<big-secret-card [secret]="secret"></big-secret-card>
</ion-slide>
</ion-slides>
However when I do this, initialSlide doesn't work. I think this might be a bug of ion-slides.
What is the best way to handle this? Skip the nice async pipe and subscribe to the FirebaseListObservable instead, and include an *ngIf="secrets.length>0" to ion-slides?
In this case do I have to use unsibscribe() when leaving the page?
Or is there any better solution?
I am now using this workaround, converting the FirebaseListObservable into a regular Observable and pre-loading with an array of 7 empty Objects. It works for my case:
getWeek(week): Observable<any> {
// get all secrets of one week into an array
let emptyWeek = [{},{},{},{},{},{},{}];
return Observable.create(observer => {
observer.next(emptyWeek);
let week$ = this.af.database.list('/dhsweek/en/week)).subscribe(result => observer.next(result));
return () => {
// unsubscribe function called automatically by async pipe when leaving page
week$.unsubscribe();
}
})
}
Using Ember cli 2.9 I'm making a simple app to convert swiss francs to euros. The app works fine manually in my browser but the integration test I've written for the converter fails. It exists as an Ember component called home-index
Template:
<h2>INPUT GOES HERE</h2>
{{input value=userInput class="user-input" placeholder="enter your francs please"}}
<button class="convert-button" {{action 'convertInput'}}>CONVERT</button>
<div class="display-output">{{outputNumber}}</div>
Component logic:
import Ember from 'ember';
export default Ember.Component.extend({
userInput: "",
outputNumber : 0,
numberifiedInput: Ember.computed('userInput', function() {
let userInput = this.get('userInput');
return parseFloat(userInput);
}),
actions: {
convertInput() {
var input = this.get('numberifiedInput');
var converted = input * 0.93;
this.set('outputNumber', converted);
}
}
});
Integration test:
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('home-index', 'Integration | Component | home index', {
integration: true
});
test('should recieve input from user and display output', function(assert) {
assert.expect(1);
this.render(hbs`{{home-index}}`);
this.$(".user-input").val("1");
this.$('.convert-button').click();
assert.equal(this.$(".display-output").text(), "0.93", "should display correct converted amount");
});
When using the app manually in the browser the value is correctly converted from 1 to 0.93 and displayed in the div. However, the integration test returns "NaN' instead of "0.93".
When I write the test into an acceptance test instead, it passes and gives the correct result. This led me to believe it was due to the use of asynchronous helpers.
I then tried to rewrite the integration test wrapped in an imported wait method as follows:
return wait()
.then(() => {
assert.equal(this.$(".display-output").text(), "0.93", "should display correct converted amount");
});
});
But still gives "NaN" as a result in the integration test.
Does anyone know why this is happening?
PS. Sorry about posting in snippets, stack overflow code blocks are being temperamental..
Try triggering the events 'input' and 'change' on the input field after setting the val.
$('.user-input').val('1');
$('.user-input').trigger('input');
$('.user-input').change();
See https://github.com/emberjs/ember.js/blob/v2.9.1/packages/ember-testing/lib/helpers/fill_in.js#L15 for how the acceptance test helper fillIn does it (but without jQuery).
If an acceptance test is acceptable, it can be a better way to test this sort of thing since the built-in helpers handle the various events that would normally be triggered by user interaction.
Recently I've made a component for recursive classifier with Relay.
I made it with two ways. One of which did not work and the second worked.
From the end-user side they both look the same with the same functionality.
In short the task is pretty much a general recursive data fetching with fetching
on next level expand.
The first way:
Fetch data after setting the relay variable expand. It is made via the
recursive fragment composition. The main part is this:
const CategoryItemContainer = Relay.createContainer(CategoryItem, {
initialVariables: {
expanded: false,
},
fragments: {
category: (variables) => Relay.QL`
fragment on Category {
id
name
hasChildren
${CategoryItemSubcategoryListContainer.getFragment('categories').if(variables.expanded)}
}
`,
},
});
const CategoryItemSubcategoryListContainer = Relay.createContainer(CategoryItemSubCategoryList, {
fragments: {
categories: () => Relay.QL`
fragment on Category {
categories(first: 10000) {
edges {
node {
${CategoryItemContainer.getFragment('category')}
}
}
}
}
`,
},
});
This way it is supposed that the Relay.RootContainer is called just once. It
did not work for me but I know that others implemented it and everything's OK (it's
a subject for another post).
The second way:
The second way worked for me easy. I don't use Relay variables but I call
Relay.Renderer for the next level of recursion depending on the UI component's
state. Like this:
{this.state.hasOwnProperty(category.id) ? // <- changing the state triggers the recursion
<Relay.Renderer
Container={CategoryItemSubCategoryListContainer}
queryConfig={new CategoryRoute({ id: category.id })}
environment={Relay.Store}
render={({ done, error, props, retry, stale }) => {
if (error) {
return 'Error';
} else if (props) {
return <CategoryItemSubCategoryListContainer {...props}
maxLevel={maxLevel}
currentLevel={currentLevel + 1}
activeCats={this.props.activeCats}
elementSelectedCallback={elementSelectedCallback}/>;
}
return (
<ul className="dropdown-menu" aria-labelledby="dLabel">
<li>
<div className="list-group-item"><p className="text-center"><img src="img/ajax-loader.gif"/></p></div>
</li>
</ul>
);
}}
/> : ''}
This works just as I expected to. Moreover I have a nice loader icon for the
next level everytime.
So, having this I've got three questions:
The first
Is any of these methods the preferrable one? Maybe there are drawbacks
somewhere that I don't know? I'd like to understand this since I'm introducing
Relay to my company and I'm willing to replace all the legacy frontend with
React and Relay.
The second
At some point in time a user will pick a category from this recursive
classifier and I will have to get the full path of data that led to the current
level.
I mean if a user picks a category at the level 3 of the hierarchy, I must give
back these data:
[root cat id, root cat name],
[level 1 cat id, level 1 cat name],
[level 2 cat id, level 2 cat name]
So when I catch the event of picking inside the component at the level 2 how can
I get the full path from the root category from Relay? I thought that I can
use some Relay API to get them from store? Or do I have to implement this
store myself?
The third
As I said in the second way of implementation there's a nice feature that I
get the loading spinner every time the Relay.Renderer is called. How can this
be done in the first way? I mean in the first way data fetching is hidden inside
fragment composition and and is triggered by the setVariables. Should I
trigger the spinner right before the setVariables method and then turn it off
in the onReadyStateChange of the only Relay.RootContainer? But then it seems
like I have to make a global store of spinners' state for rendered containers..
I'm confused here..