React 'fetch' response successful, but setState not working - reactjs

I'm new to React, working through a ".Net core CRUD with React" tutorial and trying to tweak it along the way to suit my own needs.
The page I'm dealing with here is an Add/Edit entry page. It works fine for rendering a default form with default values but doesn't render anything if the values are collected from a fetch call.
The important details are below:
interface AddPortfolioProjectDataState {
title: string;
projectData: PortfolioProject;
loading: boolean;
}
The page is told to render as follows:
public render() {
let contents = this.state.loading
? <p><em>Loading Project...</em></p>
: this.renderCreateForm(this.state.projectData.type, this.state.projectData.tech);
return (
<div>
<h1>{this.state.title}</h1>
<h3>Project</h3>
<hr />
{contents}
</div>
)
}
If I want to add a new item, therefore using a default PortfolioProject object with default values, it works fine. However, if I want to edit an old entry, I have to grab it from the server, like so:
fetch('api/StuartAitkenWebsite/GetPortfolioProject/' + projID)
.then(response => response.json() as Promise<PortfolioProject>)
.then(data => {
this.setState({ title: "Edit", loading: false, projectData: data });
});
In the debug console on Firefox, I can see the whole server process runs smoothly:
GET http://localhost:62669/api/StuartAitkenWebsite/GetPortfolioProject/2
Response payload: {"id":2,"name":"Particles Sim","projectDate":"2017-01-01T00:00:00","projectDurationWeeks":1,"type":"Desktop App","tech":"C++, SFML","views":0,"creationDate":"2018-10-22T00:00:00","modifiedDate":"2018-10-22T00:00:00","status":1}`
It gives a JSON output of the payload too, which I can't easily copy-paste here so I'll give a screenshot:
There are no server error responses, no React errors, nothing.
But that's as far as it gets.
The page remains showing 'loading', even though the data is there and ready and wants to be displayed.
From this, I can gather that the final step of the fetch call is not succeeding, because this.setState({ title: "Edit", loading: false, projectData: data }); is clearly not having any effect on the page data.
I have other fetch calls which look exactly the same but work fine. I can't see what I'm missing here.
The one and the only difference I notice is this:
When I use this component to create a fresh 'Add Project' form, the state is set like so:
this.state = {
title: "Create",
loading: false,
projectData: new PortfolioProject,
};
But when I do it from the API, it's set like so:
this.setState({
title: "Edit",
loading: false,
projectData: data
});
The successful version uses this.state, and the unsuccessful version uses this.setState
I don't know what this can mean though. As I said, no errors are being thrown, I'm sticking to the tutorial format, and it works fine in other parts of the project.
Thanks.
UPDATE
I've put a log in at the point where renderCreateForm() is called. It seems setState is actually working. Therefore, the problem must be in renderCreateForm() so I'll post that code below. Sorry it's sort of large.
private renderCreateForm(projectTypes: string, projectTech: string) {
console.log(this.state.loading); // "false"
console.log(this.state.projectData); //"Object { id:1, name: "Muon Detector".. etc
//so the render is getting the data
return (
<form onSubmit={this.handleSave}>
<div className="form-group row" >
<input type="hidden" name="Id" value={this.state.projectData.id} />
</div>
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Name">Name</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Name" defaultValue={this.state.projectData.name} required />
</div>
</div>
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="ProjectDate">Project Date</label>
<div className="col-md-4">
<input className="form-control" type="date" name="ProjectDate" defaultValue={this.state.projectData.creationDate.toDateString()} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="ProjectDurationWeeks">Project Duration (weeks)</label>
<div className="col-md-4">
<input className="form-control" type="text" name="ProjectDurationWeeks" defaultValue={this.state.projectData.projectDurationWeeks.toString()} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Type">Project Type</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Type" defaultValue={this.state.projectData.type} required />
</div>
</div >
<div className="form-group row" >
<label className=" control-label col-md-12" htmlFor="Tech">Project Tech</label>
<div className="col-md-4">
<input className="form-control" type="text" name="Tech" defaultValue={this.state.projectData.tech} required />
</div>
</div >
<div className="form-group row" >
<input type="hidden" name="Views" value={this.state.projectData.views} />
</div>
<div className="form-group row" >
<input type="hidden" name="CreationDate" value={this.state.projectData.creationDate.toDateString()} />
</div>
<div className="form-group row" >
<input type="hidden" name="ModifiedDate" value={this.state.projectData.modifiedDate.toDateString()} />
</div>
<div className="form-group row" >
<input type="hidden" name="Status" value={this.state.projectData.status} />
</div>
<div className="form-group">
<button type="submit" className="btn btn-default">Save</button>
<button className="btn" onClick={this.handleCancel}>Cancel</button>
</div >
</form>
)
}
UPDATE 2: Added some screenshots showing how things appear so far.
How the main data table page looks:
If I click 'Add New', it works:
(the 'Save' option works there too. Data posts to the server and will list on the main portfolio page)
Clicking Edit for any of the entries does not work, it gets this far:
The 'Loading Project...' text comes from the render() call for this page, as is shown in the code posted at the top of this post.
The page is supposed to look exactly like the 'Create' page (2nd screenshot), but with the title being 'Edit', and with input values populated from the given data.

The solution was absurd, but may certainly help others...
The renderCreateForm() method (as shown in Update 1 of the post) was not working because of the .toDateString() method I was using in a few of the inputs.
I changed it to .toString() and now everything works.
For example, with an input like this:
<input className="form-control" type="date" name="ProjectDate" defaultValue={projectData.creationDate.toDateString()} required />
I changed it to this:
<input className="form-control" type="date" name="ProjectDate" defaultValue={projectData.creationDate.toString()} required />
Note the defaultValue property of the input.
Repeat for all cases of .ToDateString(), and it now works,
Amazing that this didn't bring up an error anywhere. I thought Typescript and all these various frameworks were supposed to get around the issue of Javascript silently failing like that. This has been my longest and most time-wasting 'silent fail error' ever, by a very long margin.

Related

URL search parameters gets replaced - Remix Run

I'm working on a search UI where I have quite a few filters which I want as URL parameters when someone selects/checks the options. I've used the technique as advised on the Remix.run docs to come up with multiple forms within the filters. Each time a group of Filters gets submitted, the selected old parameters get disappeared. Heres my code,
<Panel header="Status" key="status">
<Form
name="search"
action='/search'
method="get"
onChange={(e) => submit(e.currentTarget, { replace: false })}
>
<ul>
<li>
<Checkbox
name="status"
value="buy_now"
defaultChecked={status.includes('buy_now')}
>
Buy Now
</Checkbox>
</li>
<li>
<Checkbox
name="status"
value="on_auction"
defaultChecked={status.includes('on_auction')}
>
On Auction
</Checkbox>
</li>
</ul>
</Form>
</Panel>
<Panel header="Price" key="price">
<Form name="search" action='/search' method="get">
<Select
name="blockchain"
value={
blockchain
? options.filter((a) => a.value === blockchain)
: undefined
}
options={options}
placeholder="Blockchain"
type="white"
/>
<div className="d-flex align-center price">
<TextInput
value={min ? min : undefined}
name="min"
placeholder="Min"
/>
<span>to</span>
<TextInput
value={max ? max : undefined}
name="max"
placeholder="Max"
/>
</div>
<button
onClick={(e) => {
e.stopPropagation()
submit(e.currentTarget, { replace: false })
}}
className="btn primary-btn btn-lg w-100"
>
Apply
</button>
</Form>
</Panel>
How Can I get around this to have all the parameters without having to manage them on my own using React state?
Edit:- I want the first filter to be submitted automatically and the latter to be submitted on a button click.
Bit of a UI of what I'm trying to achieve,
Answer: After investing enough time to look through for shortcuts, finally understood that it's not one of the magic that remix.run does. use something like formik and update the state imparatively.
When you submit a form, the only values included are the one under the submitted form. The values from any other form are not included (fortunately!).
So I'd use a single Form with all the inputs under it (checkboxes as well as text inputs).
Then instead of a onChange on the Form, you can add something like an onChange handler on the checkboxes and submit the form inside imperatively (using a ref click on the submit button or something, I think using a ref on the form you need to submit all values in the submit function so a button ref click may be simpler).
Keep in mind that if you want to "restore" the field values after submitting, you need to return them from the loader function like this:
// Loader function
const url = new URL(request.url);
return {
results: [...],
values: Object.fromEntries(url.searchParams.entries())
};
Then in the component, use values from useLoaderData:
<input type="text" name="max" defaultValue={values.max || ""}/>
Added benefit: if you come back to this page (by clicking browser back for example), your search parameters and search results are still there!
I actually put up a stackblitz for you but I lost all my changes :(
It seems like you could just keep all fields in a single form and submit that form when the submit button is pressed.
Then onChange, check if the target's name is 'status', and submit the form anyway.
export default function App() {
const submit = (form) => {
form.submit();
};
return (
<form
name="search"
action="/search"
method="get"
onChange={(e) => {
if (e.target.name === "status") {
submit(e.currentTarget);
}
}}
>
<fieldset>
<legend>status</legend>
<label>
<input type="checkbox" name="status" value="buy_now" />
buy_now
</label>
<label>
<input type="checkbox" name="status" value="on_auction" />
on_auction
</label>
</fieldset>
<fieldset>
<legend>price</legend>
<label>
<div>blockchain</div>
<select name="blockchain">
<option value="option_1">Blockchain Option 1</option>
<option value="option_2">Blockchain Option 2</option>
</select>
</label>
<label>
min <input type="number" name="min" />
</label>
<label>
max <input type="number" name="max" />
</label>
</fieldset>
<button type="submit">Apply</button>
</form>
);
}
demo
Note: not sure what your motivation is to want to separate this into separate forms, but I think the magic you're referring to is that server state, URLSearchParams, FormData and UI are all aligned because they are using the same underlying data using only common web APIs.

#material-ui/core - How to set Textarea Value

Im new to this plugin and could not find any documentation regarding my question.
I want to simply apply a value to the textarea. I started by giving it an id, but there are multiple html components that make up this textarea, ad more that ons instance of the id I added.
The value I enter gets saved, but when I reopen the modal, its suppose to load the text that was saved for that specific area.
<TextField
className="bd_textarea"
id={ `tt_description_text outlined-multiline-static`}
label="Pain description"
multiline
rows={5}
onBlur={ (f)=>(this.textOnBlur({id:answeringPainSpot.spotId, value:f.target.value, doc_name:answeringPainSpot.doc_name})) }
/>
Here are my attempts on both class and id and none worked.
$(".bd_textarea").val(spot_text);
$(".bd_textarea").next().find('textarea').eq(0).val(spot_text);
$('.bd_textarea').find('textarea').val(spot_text);
$("#tt_description_text").val(spot_text);
$("#tt_description_text").next().find('textarea').eq(0).val(spot_text);
$('#tt_description_text').find('textarea').val(spot_text);
This is the code when renderd.
<p class="MuiTypography-root MuiTypography-body1" id="modal-modal-description" sx="[object Object]">
<div class="MuiFormControl-root MuiTextField-root bd_textarea">
<label class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated" data-shrink="false" for="tt_description_text outlined-multiline-static" id="tt_description_text outlined-multiline-static-label">Pain description</label>
<div class="MuiInputBase-root MuiInput-root MuiInput-underline MuiInputBase-formControl MuiInput-formControl MuiInputBase-multiline MuiInput-multiline">
<textarea aria-invalid="false" id="tt_description_text outlined-multiline-static" rows="5" class="MuiInputBase-input MuiInput-input MuiInputBase-inputMultiline MuiInput-inputMultiline"></textarea>
</div>
</div>
</p>
Can you try setting the value like this:
<textarea
className="form-control"
rows="4"
placeholder="Add a comment"
value={this.state.newComment}
onChange={(e) => {
this.setState({
newComment: e.target.value,
);
}}>
</textarea>

Why does Cypress not recognise the testid of a react-datepicker element?

Target
To make Cypress recognise and manipulate a DatePicker element.
Problem
I cannot make Cypress recognise the data-testid of a DatePicker element, and so have no way of testing it E2E.
Error
Timed out retrying: Expected to find element: [data-testid="edit_dob"], but never found it.
Attempts
I have tried placing the DatePicker element inside input and div elements to target instead, but they fail as expected for other reasons.
I have also looked through the docs for both DatePicker and Cypress for mention of the other, and for similar questions here.
Code
The code below works as expected, it's just Cypress that seems to be incompatible with the DatePicker input.
function displayEditDetails() {
let listOfForms = [];
for (const detail in details) {
listOfForms.push(
<FormGroup controlId={detail} key={`key_edit_${detail}`}>
<FormLabel>{detail.charAt(0).toUpperCase() + detail.slice(1).replace('_', ' ')}</FormLabel>
<FormControl
data-testid={`edit_${detail}`}
type={detail}
value={details[detail] || ''}
onChange={handleDetailsChange}
/>
</FormGroup>
)
}
listOfForms.push(
<DatePicker
data-testid={'edit_dob'}
selected={dob}
onChange={setDob}
maxDate={new Date()}
/>
);
return(
listOfForms
)
}
Test
it('Displays the edit fields', () => {
cy.get('[data-testid="accountButton"]')
.click();
cy.get('[data-testid="editUserDetailsButton"]')
.click();
cy.get('[data-testid="user_email"]')
.should('not.be.visible');
cy.get('[data-testid="edit_full_name"]')
.should('be.visible');
cy.get('[data-testid="edit_dob"]')
.should('be.visible');
cy.get('[data-testid="edit_email"]')
.should('be.visible')
.should('have.value', 'test#editdetails.com');
})
Once I can target it, I plan to test clearing the current input and typing a new date in. Any help targeting the picker and warnings of further potential pitfalls are appreciated.
Edit: Here's the generated HTML of the form.
<form>
<div class="form-group">
<label class="form-label" for="email">Email</label>
<input data-testid="edit_email" type="email" id="email" class="form-control" value="admin#example.com" style="background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABHklEQVQ4EaVTO26DQBD1ohQWaS2lg9JybZ+AK7hNwx2oIoVf4UPQ0Lj1FdKktevIpel8AKNUkDcWMxpgSaIEaTVv3sx7uztiTdu2s/98DywOw3Dued4Who/M2aIx5lZV1aEsy0+qiwHELyi+Ytl0PQ69SxAxkWIA4RMRTdNsKE59juMcuZd6xIAFeZ6fGCdJ8kY4y7KAuTRNGd7jyEBXsdOPE3a0QGPsniOnnYMO67LgSQN9T41F2QGrQRRFCwyzoIF2qyBuKKbcOgPXdVeY9rMWgNsjf9ccYesJhk3f5dYT1HX9gR0LLQR30TnjkUEcx2uIuS4RnI+aj6sJR0AM8AaumPaM/rRehyWhXqbFAA9kh3/8/NvHxAYGAsZ/il8IalkCLBfNVAAAAABJRU5ErkJggg=="); background-repeat: no-repeat; background-attachment: scroll; background-size: 16px 18px; background-position: 98% 50%; cursor: auto;">
</div>
<div class="form-group">
<label class="form-label" for="full_name">Full name</label>
<input data-testid="edit_full_name" type="full_name" id="full_name" class="form-control" value="Jareth">
</div>
<div class="form-group">
<label class="form-label" for="address">Address</label>
<input data-testid="edit_address" type="address" id="address" class="form-control" value="">
</div>
<div class="form-group">
<label class="form-label" for="postcode">Postcode</label>
<input data-testid="edit_postcode" type="postcode" id="postcode" class="form-control" value="">
</div>
<div data-testid="edit_dob">
<div class="react-datepicker-wrapper">
<div class="react-datepicker__input-container">
<input type="text" class="" value="04/03/1999">
</div>
</div>
</div>
<button data-testid="confirmChangesButton" type="submit" class="LoaderButton btn btn-primary btn-block">Confirm Changes</button></form>
The problem is visible when looking at the HTML generated by running the app.
DatePicker HTML
<div data-testid="edit_dob">
<div class="react-datepicker-wrapper">
<div class="react-datepicker__input-container">
<input type="text" class="" value="04/03/1999">
</div>
</div>
</div>
The DatePicker is rendered as an input element inside a few divs, and the test id doesn't target the input.
Solution
The Selector Playground API for Cypress suggests cy.get('.react-datepicker__input-container > input'), the class of the parent div of the input. With this I can target the DatePicker, clear it, and type new input.
Notes For Next Time
Examine the HTML
Read the Docs more thoroughly
Reference the concepts used in the cy.pickDateRange command in the Cypress Real World App. It is a payment application to demonstrate real-world usage of Cypress testing methods, patterns, and workflows.
In the code for the command, you will find it is necessary to examine the HTML produced and traverse it appropriately for your needs.

Display image stored as a Path string in the Database in React

I would like to display an image in React, that is located in the src/images folder. This image has a PATH that is stored in the database and looks like this "../images/profilepic.jpg". I make my api call, get the PATH string and save it in a state called profilepic, so far this all works correctly. I then assign the state variable to a profilepic variable, which is correctly assigned when I "console.log()" it.
The issue is that I cannot show this image on the website when I use <img src{profilepic} alt="myprofilepic"/>. I have tried almost every method suggested in past questions but none work. I have tried to pass the state this.state.profilepic, with the string PATH assigned to it, to a separate component as a prop but i still run into the same issue. Using <img src=(require{profilepic}) produces an error as require will only take a string URL.
Can anyone point me in the direction of a tutorial where an image is stored as a URL/path string in a database and is called and used in React. Even if it isn't a complete project I would like to know how this can be done from the React front-end. If this is not the recommended way to deal with dynamic images in React would it then be more advisable to store the image as a BLOB in the database and then call the actual image itself in React instead of it's PATH?
The fetch call:
getUserdetails(){
const userid = this.state.userid;
axios.get(`http://localhost:5000/getuser/details?userid=${userid}`)
.then(json => {
if(json.data.success){
this.setState({
username: json.data.username,
email: json.data.email,
fullname: json.data.fullname,
company: json.data.company,
profilepic: json.data.propic
})
profilepic = this.state.profilepic
console.log(profilepic)
this.newprofile(profilepic)
}
})}
The Rendered output
return (
<div className="accout-page">
<div className="sidebar">
<Images profpic={profilepic} />
<p><input type="file" onChange={this.onchangeImage} /><button type="button" onClick={this.uploadimage}>Upload</button></p>
<p>Username: {username}</p>
<p>Full Name: {fullname}</p>
<p>Company: {company}</p>
<p>Email: {email}</p>
</div>
<div className="content">
<h1>Edit your details here</h1>
<div className="form-error-info">{updateMessage}</div>
<form>
<div className="form-group">
<label>Full Name:</label>
<input
type="text"
placeholder={fullname}
onChange={this.onNameChange} required/></div>
<div className="form-group">
<label>Company:</label>
<input
type="text"
placeholder={company}
onChange={this.onCompanyChange} required/></div>
<div className="form-group">
<label>Current Password:</label>
<input
type="password"
placeholder="enter current password"
onChange={this.onoPassChange} required/></div>
<div className="form-group">
<label>New Password:</label>
<input
type="password"
placeholder="enter new password"
onChange={this.onPassChange} required/></div>
<button className="btn btn-light" type="button" onClick={this.onSubmit}>Update</button>
</form>
</div>
</div>
)
The Images Component:
class Images extends React.Component{
render(){
const profpic = this.props.profpic;
console.log(profpic)
return(
<div>
<img src={profpic} alt="profile pic" width="100px" height="100px" />
</div>
)}}
So, basically you should have shared folder 'images' on yours server and store images there. You must be able to open your images by the path like
http://localhost:8080/images/profilepic123.jpg
After that you able to use this code snippet
<img src='/images/profilepic123.jpg' alt="myprofilepic"/>
or
<img src={profilePic} alt={profileName}/>
<img src={require(`${profilepic}`)} />
I think you just need template literals
For accessing a local image path you also need to add NODE_PATH=src in your environment variable or append it to script the start key in your package.json file as below.
"start": "NODE_PATH=src react-scripts start",
Try this :
import Image from "filePath/imgName";
You can use this Icon in any line of code down. For example:
<img src={Image}/>
Basically, you have written right code <img src={profilepic} alt="myprofilepic"/>
But the problem is that you didn't have API to open this file Like this:
const openFile = (req, res, next) => {
File.findOne({ _id: req.params.id,path:req.params.path }, function(err, users) {
if (err) {
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
});
}
console.log(users);
console.log(users.path);
});
};
//route of this controller***
router.route('/api/openfile/:id')
You can open your file
<img={`http://localhost/api/openfile/${profilepic}`}/>

Why aren't all my data validations not validating as designed?

I am developing this application in Angular 2 that has a form that you can populate and if the form is not populated, it will prompt you with what needs to be populated. It works for Customer Name, but then for the rest, for example, "a list of tools is required" does not go away once populated. Here is the code below under app/proposal/proposal-new.component.html:
<h1>Create a Proposal</h1>
<div>
<form #proposalForm="ngForm">
<div class="form-group">
<label for="customer">Customer Name</label>
<input type="text" id="customer" placeholder="ABC Supply" required name="customer" #customer='ngModel' [(ngModel)]="proposal.customer">
<div [hidden]="customer.valid || customer.pristine">
Customer name is required
</div>
</div>
<div class="form-group">
<label for="portfolio_url">Portfolio URL</label>
<input type="text" id="portfolio_url" placeholder="ABC Supply" required name="portfolio_url" #portfolio_url='ngModel' [(ngModel)]="proposal.portfolio_url">
<div [hidden]="portfolio_url.valid || portfolio_url.pristine">
A Portfolio URL is required
</div>
</div>
<div class="form-group">
<label for="portfolio_url">Tools that you'll use on the project</label>
<input type="text" id="portfolio_url" placeholder="Ruby on Rails, Angular, etc" required name="tools" #portfolio_url='ngModel' [(ngModel)]="proposal.tools ">
<div [hidden]="tools.valid || tools.pristine">
A list of tools is required
</div>
</div>
<div class="form-group">
<label for="estimated_hours">Estimated hours</label>
<input type="number" id="estimated_hours" required name="estimated_hours" #portfolio_url='ngModel' [(ngModel)]="proposal.estimated_hours ">
<div [hidden]="estimated_hours.valid || estimated_hours.pristine">
You need to enter your estimated hours for the project
</div>
</div>
<div class="form-group">
<label for="hourly_rate">Hourly rate</label>
<input type="number" id="hourly_rate" required name="hourly_rate" #portfolio_url='ngModel' [(ngModel)]="proposal.hourly_rate ">
<div [hidden]="hourly_rate.valid || hourly_rate.pristine">
You need to enter your hourly rate for the project
</div>
</div>
<div class="form-group">
<label for="weeks_to_complete">Weeks to Complete</label>
<input type="number" id="weeks_to_complete" required name="weeks_to_complete" #weeks_to_complete='ngModel' [(ngModel)]="proposal.weeks_to_complete ">
<div [hidden]="weeks_to_complete.valid || weeks_to_complete.pristine">
You need to enter the weeks you estimate to complete the project
</div>
</div>
<div class="form-group">
<label for="weeks_to_complete">Client Email <em>(Optional)</em></label>
<input type="email" id="weeks_to_complete" name="weeks_to_complete" #client_email='ngModel' [(ngModel)]="proposal.client_email">
</div>
</form>
<div>
<p>Hi {{proposal.customer}},</p>
<p>It was a pleasure getting to meet with you and review your project requirements, I believe it is a great fit for the types of applications that I build out. Please feel free to check out some of my past projects here.</p>
<p>To successfully build out the application I will be utilizing {{proposal.tools}}, and a number of other tools to ensure that the project follows industry wide best practices.</p>
<p>Ensuring that you are fully informed is one of my top priorities, so in addition to incorporating everything we discussed, I will also be creating a project management dashboard and sending you a project update message daily so that you can have a transparent view of the development as it takes place.</p>
<p>To accomplish the project and meet the requirements that we discussed, I am estimating that it will take {{proposal.estimated_hours}} hours in development time to complete. The project should take {{proposal.weeks_to_complete}} weeks to complete and with my hourly rate of {{proposal.hourly_rate}}/hour, the estimated total will be {{proposal.hourly_rate * proposal.estimated_hours}}.</p>
<p>The next step from here is to set up a meeting to finalize the project and sign contracts.</p>
<p>I am excited to working with you and build out this project.</p>
</div>
Here is the app/proposal/proposal.ts:
export class Proposal {
constructor(
public id?: number,
public customer?: string,
public portfolio_url: string = 'http://',
public tools?: string,
public estimated_hours?: number,
public hourly_rate?: number,
public weeks_to_complete?: number,
public client_email?: string,
) {}
}
If there is any other files I should be looking at and posting, please let me know. I am stumped here.
The issue you are seeing is that the local variable portfolio_url is being defined multiple times instead of a new local variable for each input. If you update the local variable definitions, which look like this: #[var_name]='ngModel', to have a unique name for each the validation should work as expected.
Update your HTML to the following:
<h1>Create a Proposal</h1>
<div>
<form #proposalForm="ngForm">
<div class="form-group">
<label for="customer">Customer Name</label>
<input type="text"
id="customer"
placeholder="ABC Supply"
required name="customer"
#customer='ngModel'
[(ngModel)]="proposal.customer">
<div [hidden]="customer.valid || customer.pristine">
Customer name is required
</div>
</div>
<div class="form-group">
<label for="portfolio_url">Portfolio URL</label>
<input type="text"
id="portfolio_url"
placeholder="ABC Supply"
required
name="portfolio_url"
#portfolio_url='ngModel'
[(ngModel)]="proposal.portfolio_url">
<div [hidden]="portfolio_url.valid || portfolio_url.pristine">
A Portfolio URL is required
</div>
</div>
<div class="form-group">
<label for="portfolio_url">Tools that you'll use on the project</label>
<input type="text"
id="tools"
placeholder="Ruby on Rails, Angular, etc"
required
name="tools"
#tools='ngModel'
[(ngModel)]="proposal.tools ">
<div [hidden]="tools.valid || tools.pristine">
A list of tools is required
</div>
</div>
<div class="form-group">
<label for="estimated_hours">Estimated hours</label>
<input type="number"
id="estimated_hours"
required
name="estimated_hours"
#estimated_hours='ngModel'
[(ngModel)]="proposal.estimated_hours ">
<div [hidden]="estimated_hours.valid || estimated_hours.pristine">
You need to enter your estimated hours for the project
</div>
</div>
<div class="form-group">
<label for="hourly_rate">Hourly rate</label>
<input type="number"
id="hourly_rate"
required
name="hourly_rate"
#hourly_rate='ngModel'
[(ngModel)]="proposal.hourly_rate ">
<div [hidden]="hourly_rate.valid || hourly_rate.pristine">
You need to enter your hourly rate for the project
</div>
</div>
<div class="form-group">
<label for="weeks_to_complete">Weeks to Complete</label>
<input type="number"
id="weeks_to_complete"
required
name="weeks_to_complete"
#weeks_to_complete='ngModel'
[(ngModel)]="proposal.weeks_to_complete ">
<div [hidden]="weeks_to_complete.valid || weeks_to_complete.pristine">
You need to enter the weeks you estimate to complete the project
</div>
</div>
<div class="form-group">
<label for="weeks_to_complete">Client Email <em>(Optional)</em></label>
<input type="email"
id="weeks_to_complete"
name="weeks_to_complete"
#client_email='ngModel'
[(ngModel)]="proposal.client_email">
</div>
</form>
<div>
<p>Hi {{proposal.customer}},</p>
<p>It was a pleasure getting to meet with you and review your project requirements, I believe it is a great fit for the types of applications that I build out. Please feel free to check out some of my past projects here.</p>
<p>To successfully build out the application I will be utilizing {{proposal.tools}}, and a number of other tools to ensure that the project follows industry wide best practices.</p>
<p>Ensuring that you are fully informed is one of my top priorities, so in addition to incorporating everything we discussed, I will also be creating a project management dashboard and sending you a project update message daily so that you can have a transparent view of the development as it takes place.</p>
<p>To accomplish the project and meet the requirements that we discussed, I am estimating that it will take {{proposal.estimated_hours}} hours in development time to complete. The project should take {{proposal.weeks_to_complete}} weeks to complete and with my hourly rate of {{proposal.hourly_rate}}/hour, the estimated total will be {{proposal.hourly_rate * proposal.estimated_hours}}.</p>
<p>The next step from here is to set up a meeting to finalize the project and sign contracts.</p>
<p>I am excited to working with you and build out this project.</p>
</div>
Working Plunkr
You shouldn't need to do this:
#portfolio_url='ngModel' [(ngModel)]="proposal.portfolio_url"
In the Angular 2 docs they say:
The hash (#) prefix to "phone" means that we're defining a phone variable
So in your case you are defining portfolio_url multiple times in your code. You do not need to use the hash prefix because the portfolio_url is already defined in your proposal object.
The following should work (read more about the template binding syntax):
<input type="number" id="hourly_rate" required name="hourly_rate" [(value)]="proposal.hourly_rate">
Or you can do this:
<input type="number" id="hourly_rate" required name="hourly_rate" #proposal.hourly_rate>
The angular 2 docs also have another example of user input: https://angular.io/docs/ts/latest/guide/user-input.html

Resources