Upgrade from Ember <3.15 to >=3.15. How do I pass form values from a controller into a component?
I cannot begin to explain the number of diagnostic combinations attempted and their corresponding errors received. So, I figure it best to ask how it should be done correctly? Is Glimmer involved?
A simple example: pass a change password from old password to both a new and confirm password via a component to a controller. In the Component, I keep getting onsubmit() is not a function error.
Code example:
User Input Form
ChangePasswordForm.hbs
<div class="middle-box text-center loginscreen animated fadeInDown">
<div>
<h3>Change Password</h3>
<form class="m-t" role="form" {{on "submit" this.changePassword}}>
{{#each errors as |error|}}
<div class="error-alert">{{error.detail}}</div>
{{/each}}
<div class="form-group">
{{input type="password" class="form-control" placeholder="Old Password" value=oldPassword required="true"}}
</div>
<div class="form-group">
{{input type="password" class="form-control" placeholder="New Password" value=newPassword required="true"}}
</div>
<div class="form-group">
{{input type="password" class="form-control" placeholder="Confirm Password" value=confirmPassword required="true"}}
</div>
<div>
<button type="submit" class="btn btn-primary block full-width m-b">Submit</button>
</div>
</form>
</div>
</div>
Template Component
ChangePassword.hbs
<Clients::ChangePasswordForm #chgpwd={{this.model}} {{on "submit" this.changePassword}} #errors={{this.errors}} />
Component
ChangePasswordForm.js
import Component from '#glimmer/component';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class ChangePasswordForm extends Component {
#tracked oldPassword;
#tracked newPassword;
#tracked confirmPassword;
#tracked errors = [];
#action
changePassword(ev) {
// Prevent the form's default action.
ev.preventDefault();
this.oldPassword = ev.oldPassword;
this.newPassword = ev.newPassword;
this.confirmPassword = ev.confirmPassword;
// Call the form's onsubmit method and pass in the component's values.
this.onsubmit({
oldPassword: this.oldPassword,
newPassword: this.newPassword,
confirmPassword: this.confirmPassword
});
}
}
Controller
ChangePassword.js
import Controller from '#ember/controller';
import { inject as service } from '#ember/service';
import { action } from '#ember/object';
export default class ChangePassword extends Controller {
#service ajax
#service session
#action
changePassword(attrs) {
if(attrs.newPassword == attrs.oldPassword)
{
this.set('errors', [{
detail: "The old password and new password are the same. The password was not changed.",
status: 1003,
title: 'Change Password Failed'
}]);
}
else if(attrs.newPassword != attrs.confirmPassword)
{
this.set('errors', [{
detail: "The new password and confirm password must be the same value. The password was not changed.",
status: 1003,
title: 'Change Password Failed'
}]);
}
else
{
let token = this.get('session.data.authenticated.token');
this.ajax.request(this.store.adapterFor('application').get('host') + "/clients/change-password", {
method: 'POST',
data: JSON.stringify({
data: {
attributes: {
"old-password" : attrs.oldPassword,
"new-password" : attrs.newPassword,
"confirm-password" : attrs.confirmPassword
},
type: 'change-passwords'
}
}),
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/vnd.api+json',
'Accept': 'application/vnd.api+json'
}
})
.then(() => {
// Transistion to the change-password-success route.
this.transitionToRoute('clients.change-password-success');
})
.catch((ex) => {
// Set the errors property to the errors held in the ex.payload.errors. This will allow the errors to be shown in the UI.
this.set('errors', ex.payload.errors);
});
}
}
}
Model
ChangePassword.js
import Route from '#ember/routing/route';
import AbcAuthenticatedRouteMixin from '../../mixins/abc-authenticated-route-mixin';
export default Route.extend(AbcAuthenticatedRouteMixin, {
//export default class ChangePasswordRoute extends Route(AbcAuthenticatedRouteMixin, {
model() {
return {
oldPassword: '',
newPassword: '',
confirmPassword: ''
};
},
})
There is no onsubmit method in #glimmer/component, so you cannot call this.onsubmit inside an action in the component.
First, you need to pass the action created in your controller to your component. This can be done like this:
<ChangePasswordForm #chgpwd={{this.model}} #changePassword={{action 'changePassword'}} />
Remember, you cannot pass data up any more in a glimmer component, you need to use an action since everything is one way binding.
Second, you need to call this action inside your glimmer component:
this.args.changePassword({
oldPassword: this.oldPassword,
newPassword: this.newPassword,
confirmPassword: this.confirmPassword
});
I've created an Ember Twiddle for you to show this example working.
Related
In my InertiaJS/VueJS project I have a prop that receive some data from the backend:
event: {
type: Object,
default: () => { return {} }
},
That's how the event obj looks in the backend:
['name' => 'Event Name']
I use toRefs to convert the reactive prop and update its properties in the UI:
const eventRef = toRefs(props).event
So the Event has the name 'Event Name' when the component loads, when I update the event name in the UI to 'New Name' and submit the form, I send the eventRef obj in the request to create the new event:
Inertia.post(url, eventRef, only: ['global'])
If there's a validation error in the backend, I return it to the frontend and show the error in the UI (This is working without problems). The problem I have is that Inertia (or maybe VueJS) is returning the object eventRef to his previous state when the component is created. Which means that the name property of the eventRef changes to 'Event Name' again, instead of staying with 'New Name` that was updated in the UI. I would like to preserve the state of the object after I submit the form. This is my Inertia response:
component: "Events/EventNew"
props: {
global: {} // Global object
}
url: "/app/qa/events/new"
version: null
As you can see I'm not even getting the 'event' prop from the backend, so it shouldn't be updated. After reading Inertia docs I thought that a simple preserveState:true in the request options would do the job but this is not happening. Every time the server returns an Inertia response, the eventRef obj is 'reset'.
What am I missing here? I would appreciate some help
I believe I had the same problem using Inertia with Vue2. If I understood correctly, you probably seeing this on a form where you trying to update and entry, right? Your validation is working but the form keeps resetting itself to the previous state. If that's the case, what solved this for me was this:
Instead of using Inertia.post() directly, use the Inertia Form Helper instead
Vue 2
<template>
<form #submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
<script>
export default {
data() {
return {
form: this.$inertia.form({
email: null,
password: null,
remember: false,
}),
}
},
}
</script>
Vue 3
<template>
<form #submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
<script>
import { useForm } from '#inertiajs/inertia-vue3'
export default {
setup () {
const form = useForm({
email: null,
password: null,
remember: false,
})
return { form }
},
}
</script>
I solved the problem, it was the toRefs that was modifying the props in the component after the request was sent. Using a reactive object was the solution:
const eventRef = reactive(props.event)
I have a simple registration form in Laravel 8 using Vue js where I need to check first if the user who refers the person registering exists in my database prior to submitting. if a record exists, I need to dynamically display the user's full name in the input field on the #change event.
Here's my Vue component:
<template>
<div>
<h2>TESTING</h2>
<form #submit.prevent="submit" >
<input type="text" class="form-control" v-model="form.ucode" #change="getUser()">
<input type="text" class="form-control" v-model="form.uname">
</form>
</div>
</template>
<script>
export default {
data: function(){
return{
form: {
ucode: "",
uname: "",
},
}
},
methods:{
getUser(){
axios.get('api/checkUser?ucode=' + this.form.ucode).then(res=>{
this.form.uname = res.data.first_name
})
}
}
}
Here's my ResellerController and API route:
Route::get('/checkUser', [ResellerController::class, 'show']);
public function show()
{
$ucode = request('ucode');
$user = DB::table('resellers')->where('username', $ucode)->select('id', 'first_name')->get();
return response()->json($user);
}
I think I don't have issues with my controller because it returns back the correct JSON data
[{"id":1,"first_name":"William Hardiev"}]
But when I test my code, uname is undefined.
form:Object
ucode:"williambola_05"
uname:undefined
Can anyone help me with this?
You issue is the JSON response that you receive from the server. You are getting a JSON Array from the server, whereas your JS code is handling a JSON object
You can handle it like this:
<template>
<div>
<h2>TESTING</h2>
<form #submit.prevent="submit">
<input
type="text"
class="form-control"
v-model="form.ucode"
#change="getUser()"
/>
<input type="text" class="form-control" v-model="form.uname" />
</form>
</div>
</template>
<script>
import axios from "axios";
export default {
data: function() {
return {
form: {
ucode: "",
uname: ""
}
};
},
methods: {
getUser() {
axios.get("api/checkUser/?ucode=" + this.form.ucode).then(res => {
this.form.uname = res.data[0].first_name;
});
}
}
};
</script>
OR you can just change the get query on the server side to simply return a single JSON object rather than an array and your js code should automatically start working:
$user = DB::table('resellers')
->where('username', $ucode)
->select('id', 'first_name')
->first();
In my vue app I have two components one which is a form that posts the form data to my api. And the other gets and displays these posts in a section on the page. My issue is that when I submit a new post the posts lists aren't updated. The data stays the same unless I refresh the page. How can I get my posts list to update when I submit the form?
My Code:
client/src/App.vue
<template>
<div id="app">
<MainHeader :modalVisability="modal" v-on:showModal="toggleModal" />
<div id="content_wrap">
<Summary />
</div>
<OppForm :modalVisability="modal" />
</div>
</template>
<script>
import MainHeader from './components/MainHeader.vue';
import OppForm from './components/oppForm.vue';
import Summary from './components/Summary.vue';
export default {
name: 'App',
components: {
MainHeader,
Summary,
OppForm
},
data () {
return {
modal: false
}
},
methods: {
toggleModal (modalBool) {
this.modal = modalBool;
}
}
}
</script>
client/src/components/oppForm.vue
<template>
<div id="opp_form_modal" >
<form #submit.prevent="SubmitOpp" v-if="modalVisability">
<input type="text" name="company_name" v-model="company_name">
<button type="submit">Submit</button>
</form>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'oppForm',
props: {
modalVisability: Boolean,
},
data () {
return {
company_name: ''
}
},
methods: {
SubmitOpp () {
axios.post('http://localhost:5000/', {
company_name: this.company_name,
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
}
}
</script>
client/src/components/Summary.vue
<template>
<div id="summary_section">
<h2>Summary</h2>
<div id="summary_board">
<div class="column">
<div class="head">
<h3>Opportunities</h3>
</div>
<div class="body">
<div class="post"
v-for="(post, index) in posts"
:key="index"
>
<p class="company">{{ post.company_name }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return{
posts: []
};
},
created() {
axios.get('http://localhost:5000/')
.then(res => {
// console.log(res);
const data = res.data;
this.posts = data;
})
.catch(error => console.log(error));
}
}
</script>
The problem is that you're actually fetching your posts only on the app creation (i.e. inside the created() method).
You should wrap your axios call inside a function updatePosts() and then call it whenever you add a new post successfully, or you could create a custom event that is triggered whenever a new post is added.
created() is called only once (see vue lifecycle) so you fetch API before submitting form.
Try to add some console.log to understand what is called when.
You could use an global event bus and send form value as event data to summary. I could imagine also a solution where event is used to "tell" summary that form was submitted (just boolean, not data itself). In summary you then call API each time you receive event.
Or simple add an "update" button to summary to manually call API.
See Communication between sibling components in VueJs 2.0
or global vue instance for events for detailed examples.
I have a form for updating the details of alumni in ember using RESTful api. Is it possible to prevent the form from auto filling the data I previously entered in the form corresponding to another record in the model?
I have these codes in my update route directory(I am using pod-structure):
controller.js
# app/alumnis/update/controller.js
import Controller from '#ember/controller';
import { get, set } from '#ember/object';
export default Controller.extend({
firstName: null,
actions: {
updateAlumni(value) {
let firstName = get(this, 'firstName');
if(firstName) {
firstName = firstName.charAt(0).toUpperCase() + firstName.slice(1).toLowerCase();
this.get('store').findRecord('alumni', value).then(function(alumni) {
alumni.set('firstName', firstName);
alumni.save();
});
}
this.transitionToRoute('alumnis.show', value)
},
},
});
route.js
# app/alumnis/update/route.js
import Route from '#ember/routing/route';
import { set } from '#ember/object';
export default Route.extend({
model(params) {
return this.store.findRecord('alumni', params.id);
},
setupController(controller, model) {
set(controller, 'alumni', model);
}
});
template.hbs
# app/alumnis/update/template.hbs
<form class="alumniForm" {{action "updateAlumni" on="submit"}}>
<div class="form-group">
<h3>First Name : {{input name="firstName" type="text" value=firstName placeholder=alumni.firstName autocomplete="off"}}</h3>
</div>
<button class="btn btn-primary" {{action "updateAlumni" alumni.id}}>Submit</button>
</form>
router.js
# app/router.js
import EmberRouter from '#ember/routing/router';
import config from './config/environment';
const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('alumnis', function() {
this.route('show', {path: '/:id'});
this.route('add', {path: '/add'});
this.route('update', {path: '/:id/update'});
});
});
export default Router;
On the first rendering of update route after every reloading, no form fields are filled automatically. But, once we enter data to the firstName input field, it is rendered to form field in update page of any other record in the model alumni.
Properties that are set in a controller in ember will remain set when you re-navigate to the page.
The logic you've shown, leads me to believe you don't even need the controller. You are modifying a model property, saving it and transitioning.
You were doing a round-about way of updating the record, The alumni record was your model, yet you were trying to re-fetch it from the store.
route.js
# app/alumnis/update/route.js
import Route from '#ember/routing/route';
import { set,get } from '#ember/object';
export default Route.extend({
model(params) {
return this.store.findRecord('alumni', params.id);
},
updateAlumni() {
let changedAttrs = get(this, 'model').changedAttributes();
if (changedAttrs.firstName) {
let firstName = get(this, 'model.firstName').toLowerCase().capitalize();
set('model.firstName', firstName);
}
get(this,'model').save()
this.transitionToRoute('alumnis.show', get(this,'model'))
}
});
template.hbs
# app/alumnis/update/template.hbs
<form class="alumniForm" {{action "updateAlumni" on="submit"}}>
<div class="form-group">
<h3>First Name : {{input name="firstName" type="text" value=model.firstName placeholder=alumni.firstName autocomplete="off"}}</h3>
</div>
<button class="btn btn-primary" {{action "updateAlumni"}}>Submit</button>
</form>
I was able to resolve the issue by changing the below codes:
controller.js
# app/alumnis/update/controller.js
import Controller from '#ember/controller';
import { get, set } from '#ember/object';
export default Controller.extend({
firstName: null,
actions: {
updateAlumni(value) {
let alumni = get(this, 'alumni');
let changedAttrs = alumni.changedAttributes();
if(changedAttrs.firstName) {
let firstName = alumni.firstName.toLowerCase().capitalize();
alumni.set('firstName', firstName);
alumni.save()
}
this.transitionToRoute('alumnis.show', value)
},
},
});
template.hbs
# app/alumnis/update/template.hbs
<form class="alumniForm" autocomplete="off" {{action "updateAlumni" on="submit"}}>
<div class="form-group">
<h3>First Name : {{input name="firstName" type="text" value=alumni.firstName}}</h3>
</div>
<button class="btn btn-primary" {{action "updateAlumni" alumni.id}}>Submit</button>
</form>
No change in app/alumnis/update/route.js
I've built two components: signup and signup-form.
.
After filling up the form and clicking the Submit button I get:
I've searched through all the questions with similar problem and none of them helped me fix the blocker.
Signup.component.html has the sign-up form component:
(...)
<app-signup-form></app-signup-form>
(...)
Signup-form.component.html:
<form [formGroup]="mysignup">
<div class="form-group">
Name:<br>
<input type="text" formControlName="name" ngModel="name" required/>
</div>
<div class="form-group">
Email:<br>
<input type="email" formControlName="email" ngModel="email" required/>
</div>
<div class="form-group">
Password:<br>
<input type="password" formControlName="password" ngModel="password" required/>
</div>
<div class="form-group">
<input type="button" value="Submit" class="btn btn-primary" (click)="signupUser()" />
</div>
<br><br>
<pre>{{mysignup.value | json}}</pre>
</form>
Signup-form.component.ts:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, Validators } from '#angular/forms';
import { FormGroup, FormControl } from '#angular/forms';
import { ReactiveFormsModule } from '#angular/forms'; // Linking the form model to the form template
import { HttpClient, HttpResponse } from '#angular/common/http';
#Component({
selector: 'app-signup-form',
templateUrl: './signup-form.component.html',
styleUrls: ['./signup-form.component.scss']
})
export class SignupFormComponent implements OnInit {
mysignup: FormGroup;
constructor() {}
ngOnInit() {
this.mysignup = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', Validators.required),
password: new FormControl('', Validators.required)
});
}
signupUser($scope, $http) {
this.mysignup = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', Validators.required),
password: new FormControl('', Validators.required)
});
// tslint:disable-next-line:no-debugger
// debugger;
$http.post('test.php', {
'name': $scope.name,
'email': $scope.email,
'password': $scope.mysignup.password
}).then(function(Response) {
console.log('Data inserted successfully');
}, function(error) {
alert('Sorry! Data couldnt be inserted!');
console.error(error);
});
}
}
I just want the data to arrive test.php because from there onward I know how to handle.
How to fix this?
You dont have http in your code. Change your constructor to this:
First, import http like this: import { HttpClient } from "#angular/common/http";
Then declare a local variable named http in your constructor like this:
constructor(private http: HttpClient) { }
Also, it's a standard practice to use http instead of $http.
And do NOT pass $http and $scope to signupUser function. Within signupUser function access the form values like this:
this.formGroupName.controls['formControlName'].value (In your case this.mysignup.controls['name'].value).
Then, you can call the signupUser from html like:
<button (click)="signupUser()">Sign UP<button>
You'll have to change your post call like this (NOTE: http.post returns an Observable so you'll have to subscribe to it like this):
this.http.post('test.php', {
'name': this.mysignup.controls['name'].value,
'email': this.mysignup.controls['email'].value,
'password': this.mysignup.controls['password'].value
}).subscribe(function(Response) {
console.log('Data inserted successfully');
}, function(error) {
alert('Sorry! Data couldnt be inserted!');
console.error(error);
});
Please note: It is advisable to write all the HttpClient related(get, post, etc..) codes separately in service file.
Looks like you are too much in old Angular 1.x patterns there. The error comes from the line with $http.post, because you injected the $http service in the signupUser method. You will need to inject it into the constructor of your component class
constructor(protected $http: HttpClient) {}
and then access it in the class' methods with
this.$http.post(...)
Don't forget to import CommonModule in the #Module declaring this component.
More info about dependency injection in the Angular docs: https://angular.io/guide/dependency-injection-pattern
Also consider to overthink your variable naming, because the $ sign is not used for this case in Angular 2+, and don't use $scope, but data bindings.