Angular, Typescript, and .net core - updating SolarProjection with a form

Angular, Typescript, and .net core - updating SolarProjection with a form

I’m close to wrapping up my initial pass through the solar projection feature of the website. One of the final touches is the ability for visitors to run their own projections.

Currently, other programmers can go about doing the above by grabbing the NuGet package, but I’d like to offer something on the page itself.

A few things we’ll need:

  • Form entry inputs for providing all necessary information related to a solar projection
  • New typescript objects to represent the new form, as well as its submission functionality
  • New .net core API endpoint to support the new form (previously there was a parameterless endpoint that returned values specific to my projection)

Note all full code updates will be present at the bottom in a gist, most of the other code will just be snippets and in code blocks, since gist support can only do a full post, rather than individual files (and creating 40 billion gists for a post seems excessive).

I generally like creating my model classes and interfaces first when designing something new That being said, I believe it would make sense to start with the form parameters that will be necessary for both our form entry, and our new API endpoint.

Given the current page:

We’ll need a few inputs:

  • Number of years to project into the future
  • The utility/solar array kw/h/year (in other words, a solar array that will theoretically cover 100% of your generation). In my case this value was ~17,000 kw/h/year
  • solar cost per month — given a solar array of z kw/year the cost per month (note I did not have this number until we talked about them with Vivint — in my case it’s $189 a month
  • solar finance years — number of years the mortgage is active — 20 years for me.
  • utility cost full year — the total yearly cost for the year specified in the utility/solar array kw/h/year bullet point above
  • utility percent increase per year — the estimated percentage increase per kw/h from the solar company — I used 3% (0.03)

The above can be represented in the TypeScript class:

1
2
3
4
5
6
7
8
9
10
11
export class SolarProjectionFormModel {

constructor(
public yearsToProject: number,
public utilitySolarArrayKwhYear: number,
public solarCostPerMonth: number,
public solarFinanceYears: number,
public utilityCostFullYear: number,
public utilityPercentIncreasePerYear: number
) { }
}

and c# class:

1
2
3
4
5
6
7
8
9
public class SolarProjectionParameters
{
public int YearsToProject { get; set; }
public int UtilitySolarArrayKwhYear { get; set; }
public double SolarCostPerMonth { get; set; }
public int SolarFinanceYears { get; set; }
public double UtilityCostFullYear { get; set; }
public double UtilityPercentIncreasePerYear { get; set; }
}

Next, we want to work on adding our form, I used Angular Template-driven forms as the baseline tutorial to follow on figuring this out (I’ve never done this before).

We want to start a form as so:

1
<form *ngIf="!isExpandedForm" (ngSubmit)="onSubmit()" #projectionForm="ngForm">

In the above, you can see I’m only showing the form when !isExpandedForm, as to hide the form with a button click, and automatically when retrieving a new set of results. Additionally, the above shows that on submit, we are to invoke the onSubmit() function — this functions definition will come into play for posting to our api, and is handled in some to be defined typescript code.

The basic template for each form “item” will be (starting with the first as sample):

1
2
3
4
5
<div class="form-group">
<label for="yearsToProject">Years to project</label>
<input type="number" class="form-control" id="yearsToProject" name="yearsToProject" required [(ngModel)]="model.yearsToProject"
/>
</div>

The above is applied to all properties of our form model, all properties are numbers, and required.

Next, our submit button:

1
2
3
<button type="submit" class="btn btn-success" [disabled]="!projectionForm.form.valid">
Submit
</button>

In the above, you can see it’s pretty standard, but using a nice angular (directive?) the only enables the submit button the form is valid (in this case all fields are required, and must be specified as numbers).

Typescript updates are next:

we have a few new imports we’ll need to make use of:

1
import { HttpHeaders, HttpParams } from '@angular/common/http';

This will give us access to HttpHeaders and HttpParams . Both of these imports will be used for passing around our new SolarProjectionFormModel to our api.

The constructor is updated to take in an HttpClient and BASE_URL to save as instance members, for use in the api get later.

1
2
3
4
constructor(http: HttpClient, [@Inject](http://twitter.com/Inject)('BASE_URL') baseUrl: string) {
this.http = http;
this.baseUrl = baseUrl;
}

Set an initial state of our model (matching my solar projection) in an instance var:

1
2
3
4
5
6
7
8
9
model: SolarProjectionFormModel =
new SolarProjectionFormModel(
25,
17000,
180,
20,
2240,
.03
);

and now for the meat and potatoes of the class, the new get function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
getProjection() {
let headers = new HttpHeaders({
'Accept': 'application/json',
'Content-Type': 'application/json'
});

let params = new HttpParams()
.set("param", JSON.stringify(this.model));

this.http.get<SolarProjection>(
this.baseUrl + 'api/SolarProjection',
{
headers: headers,
params: params
}
)
.subscribe(
result => {
this.solarProjection = result;
},
error => {
console.error(error)
}
);
}

In the above, we’re doing a few new things, setting out HttpHeaders to indicate we’re passing JSON, and setting up some HttpParams to contain a jsonified version of our model.

The above function os what does the actual get for our projection, it’s called in a few places, one of which being on init, and also as a part of the onSubmit() function described earlier:

1
2
3
4
5
6
7
8
onSubmit() {
this.toggleFormInput();
this.getProjection();
}

toggleFormInput() {
this.isExpandedForm = !this.isExpandedForm;
}

Notice how onSubmit() calls both toggleFormInput() (to hide the criteria form), and getProjection()

Finally, time for the new .net core API endpoint that will handle our parameterized get.

I am not a fan of how I implemented this, I feel like I should be wrapping both the parameters and result in an object to convey status and types… it just currently feels a bit yucky — perhaps I’ll pursue that next.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// <summary>
/// Get solar projection with a json
/// string representing <see cref="SolarProjectionParameters"/>.
///
/// TODO there's gotta be a better way to do this signature,
/// both from a param and return perspective. I feel like
/// parameter should convey the proper type, and the result
/// should be able to convey a status perhaps?
/// </summary>
/// <param name="param">Json representation of <see cref="SolarProjectionParameters"/></param>
/// <returns><see cref="SolarVsUtilityProjection"/></returns>
[HttpGet]
public SolarVsUtilityProjection Get(string param)
{
// Convert json string to type
var solarProjectionParameters = JsonConvert
.DeserializeObject<SolarProjectionParameters>(param);

// Calculate future projection
return _service.CalculateFutureProjection(
solarEstimate: new YearlyKwhUsageFromAnnual(
totalCost: solarProjectionParameters.SolarCostPerMonth * 12,
totalKiloWattHours: solarProjectionParameters.UtilitySolarArrayKwhYear
),
projectionParameters: new ProjectionParameters(
utilityYear: new YearlyKwhUsageFromAnnual(
totalCost: solarProjectionParameters.UtilityCostFullYear,
totalKiloWattHours: solarProjectionParameters.UtilitySolarArrayKwhYear
),
yearsToProject: solarProjectionParameters.YearsToProject,
financeYears: solarProjectionParameters.SolarFinanceYears,
percentIncreasePerYear: solarProjectionParameters.UtilityPercentIncreasePerYear
)
);
}

In the above, I’m taking in a string that represents my model type ProjectionParameters defined earlier. Converting that string into its object representation, then calling my projection service. I don’t know what happens yet if bad data is sent in, but I feel like I need to take a look at this implementation in a number of ways already pointed out previously.

That should do it! I did a few other minor things here and there not pointed out, but the entirety of the source can be found:

Kritner/KritnerWebsite

Page now looks like:

And gists of (most) of the files from this post:

Getting some of this working was a bit challenging coming from someone whose never worked with angular forms and very little with web api — so hopefully this will help someone else out eventually! :)

Related:

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×