I am learning ember js from last couple of weeks, and building an application to learn it. I am in a situation where I have to build a dynamic form which will be bind to ember model. (the simplest example for this problem could be nested form, where we can click on add more link/button to add form on the fly, and add values to them).
But for me, I am building survey like site, where we can have lots of option to select and user can select one of the option from available one:
what I have done so far?
readAnswer: Ember.computed(function() {
return this.get('store').query('answer', { filter:
{
question_id: this.get('question.id'),
submission_id: this.get('submission.id')
}
})
}),
buildAnswer: Ember.computed(function() {
this.get('store').createRecord('answer', {
question: this.get('question'),
submission: this.get('submission')
})
}),
answer: Ember.computed(function() {
const ans = this.get('readAnswer');
console.log(`question_id: ${this.get('question.id')}, submission_id: ${this.get('submission.id')}`);
if(Ember.isEmpty(ans)) {
return this.get('buildAnswer');
} else {
return ans;
}
})
answer.hbs
<div class="row col-sm-12">
<div class="form-group">
<label>{{model.title}}</label>
<p>
{{input type="text" value=answer.field01 class="form-control" placeholder="width"}}
</p>
<p>
{{input type="text" value=answer.field02 class="form-control" placeholder="depth"}}
</p>
</div>
</div>
NOTE here answer.hbs is a component, and these are call recursively (in loop) from parent route. So for 2 questions, we can have 4 textboxes, 2 text box for each question, first textbox for answer.field01 and second textbox for answer.field02
Let's say I have 2 questions, till now, I can see 2 answers build in the ember store if they don't already exists in database and then, I can see 4 textboxes generated in view. But they are not binding. Meaning, if I can value of the textbox, nothing happens in the ember store.
Expected Result
When I input answer in the textbox, it should bind with answer.fieldxx properly.
I extracted those codes from computed property to init() function and everything works now:
answer: null,
init() {
this._super(...arguments);
// build without computed property
this.get('store').query('answer', { filter:
{
question_id : this.get('question.id'),
submission_id : this.get('submission.id')
}
}).then((answers) => {
if(Ember.isEmpty(answers)) {
let a = this.get('store').createRecord('answer', {
question : this.get('question'),
submission : this.get('submission')
})
this.set('answer', a); // build new object and set answer
} else {
this.set('answer', answers.get('firstObject')); // get first answer and build it (because it is always 1 record)
}
}, (reason) => {
console.log(reason);
});
},
Related
Using the Ember addon ember-bootstrap I can make a set of radio buttons like this :
{{form.element controlType="radio" label="Fruit Type" property="radio" options=radioOptions optionLabelPath="label"}}
with a Controller that looks like this :
export default Controller.extend({
init() {
this._super(...arguments);
this.radioOptions = [
{
label: 'Citrus',
value: 'C',
inline: true
},
{
label: 'Non-Citrus',
value: 'N',
inline: true
}
];
}
});
The relevant doco is here https://www.ember-bootstrap.com/#/components/forms .
However what I can't do is provide a custom value to each radio button so that I end up with rendered HTML like this :
<label>Citrus</label>
<input type="radio" value="C">
<label>Non-Citrus</label>
<input type="radio" value="N">
I have looked at "Custom Controls" on https://www.ember-bootstrap.com/#/components/forms but I can't see how that applies to this case.
EDIT: Just to be clearer about why I want to do this I want to display the readable label (eg "Citrus") but have the non-readable value ("C") available to send back to the server (because the server thinks in terms of "C" or "N".
It's not essential I could send "Citrus" back and map it around on the server but I just thought this would be very straightforward.
Looking at the part of the doco starting with "You can also just customize the existing control component:" on https://www.ember-bootstrap.com/#/components/forms it does seem like you should be able to do the sort of thing I'm after but the example shown doesn't address the use of a value attribute and I can't figure out how to .
You don't need to have the HTML rendered like that. if you want to access the checked radio, simply it is the property name dot value like radio.value.
Here how to get it in the on submit action:
actions: {
onSubmit() {
alert(this.radio.value)
}
}
I had exactly the same issue but I finally solved it. You have to use form.element in block mode. Why is it also necessary to also write an action to update the value? I have no idea!
In my implementation, I'm using Ember changesets and my property is called usageType. I hope it's clear enough to adapt for your needs.
I'm also using Ember Truth Helpers, which is what the {{eq opt.value el.value}} part is. It sets checked to true if the input value is equal to the current-selected value.
# my-component.js
actions: {
selectUsageType(option) {
return this.set('changeset.usageType', option);
}
}
# usage-type-options.js (imported in component JS)
[{
label: 'First label',
value: 'A'
},
{
label: 'Second label',
value: 'B'
}]
# Etc.
# my-component.hbs
# I'm not using angle-bracket invovation here, oops
{{#form.element
property="usageType"
label="My label" as |el|
}}
{{#each usageTypeOptions as |opt|}}
<div class="form-check">
<input
type="radio"
class="form-check-input"
id="{{el.id}}-{{opt.value}}"
checked={{eq opt.value el.value}}
onchange={{action "selectUsageType" opt.value}}
>
<label for="{{el.id}}-{{opt.value}}" class="form-check-label">
{{opt.label}}
</label>
</div>
{{/each}}
{{/form.element}}
I want to have a search field inside a component that can be placed anywhere in the app. It can appear on any template, or nested in components. The search form would accept user input (search term) and submit would trigger a search action which transitions to a results template.
Seems simple enough, but I can't figure out how to make an action globally available. And if I could, how do you pass the inputted term to the action in the first place? There's surprisingly little info on how to handle form submits with Ember CLI.
Thus far I've just been submitting a regular form with action='/results'. But that's obviously reloading the app.
I've been messing with creating an action in the index controller like this:
export default Ember.Controller.extend(defaultParams, {
term: '',
actions: {
keywordSearch() {
this.transitionToRoute('results', { queryParams: { q: this.get('term') }});
}
}
});
Then passing a closure action down to my search component, which is nested 2 deep from the index template.
index.hbs:
{{index-search keywordSearch=(action "keywordSearch")}}
index-search.hbs (component):
{{search-field keywordSearch=keywordSearch }}
search-field.hbs (nested component):
<form {{ action (action keywordSearch) on='submit' }}>
{{ input value=term }}
<button type="submit">Search</button>
</form>
And that will run the action, but the term is not supplied. How do you supply term to the closure action?
And...do I really need to pass the action down to every single place the search field is going to appear in the app, or is there an easier way to do it?
Instead of writing actions in all components and routes, you can create a service for search. Inject the service into the component and handle the route transition from service method. Check the sample code below,
Search-component.hbs
<form {{ action (action search) on='submit' }}>
{{ input value=keyword }}
<button type="submit">Search</button>
</form>
Search-component.js
export default Ember.Component.extend({
globalSearch: Ember.inject.service('search'),
actions: {
search() {
const { keyword } = this.getProperties('keyword');
this.get('globalSearch').showResults(keyword).then(() => {
alert('Success');
}, (err) => {
alert('Error while searching: ' + err.responseText);
});
}
}
});
Service - app/services/search.js
import Ember from 'ember';
export default Ember.Service.extend({
init() {
this._super(...arguments);
},
showResults(keyword) {
// write code for transition to search results route here
}
});
I'm using the ng-bootstrap typeahead component to search a customer database. When the user selects a customer from the typeahead results list, I navigate to a customer details page. I've got this working, but I want to clear the input field after navigation has taken place. I've tried setting the model to null or an empty string in the selectItem event logic, but this isn't working:
customer-search-typeahead.component.html
<template #resultTemplate let-r="result" let-t="term">
<div>
<div>
{{r.resource.name[0].given}} {{r.resource.name[0].family}}
</div>
<div>
{{r.resource.birthDate | date: 'dd/MM/yyyy'}}
</div>
</div>
</template>
<input type="text" class="form-control" [resultTemplate]="resultTemplate" (selectItem)="onSelect($event)"
[(ngModel)]="model" placeholder="Start typing a customer name..." [ngbTypeahead]="search"/>
customer-search-typeahead.component.ts
#Component({
selector: 'customer-search-typeahead',
template: require('./customer-search-typeahead.component.html'),
styles: [`.form-control { width: 300px; }`]
})
export class CustomerSearchTypeaheadComponent {
model: any;
searching: boolean;
constructor(private customerService: CustomerService, private router: Router) {}
onSelect($event) {
this.router.navigate(['/customers', $event.item.resource.id]);
this.model = null;
};
search = (text$: Observable<string>) =>
//omitted for brevity
}
The typeahead input looks like this after a selection has been made:
Solution
customer-search-typeahead.component.html
<input type="text" class="form-control" #input [ngbTypeahead]="search" (selectItem)="onSelect($event); input.value='' ">
customer-search-typeahead.component.ts
onSelect($event, input) {
$event.preventDefault();
this.router.navigate(['/customers', $event.item.resource.id]);
};
The issue you witnessing arises from the fact that the NgModel directive is updating model binding asynchronously and the actual model is updated after the onSelect method gets executed. So your model update gets overridden by the NgModel functionality.
Fortunately we (ng-bootstrap authors) got all the flex points in place to cover your use-case :-) There are a couple of things that you could do.
Firstly the $event object passed to the onSelect method has the preventDefault() method and you can call it to veto item selection (and as a result writing back to the model and input field update).
$event.preventDefault() will make sure that the model is not updated and the input field is not updated with the selected item. But text entered by a user will still be part of the input so if you want to clear up this as well you can directly update the input's value property.
Here is code demonstrating all those techniques together:
onSelect($event, input) {
$event.preventDefault();
this.selected.push($event.item);
input.value = '';
}
where input argument is a reference to the input DOM element:
<input type="text" class="form-control" #input
[ngbTypeahead]="search" (selectItem)="onSelect($event, input)">
Finally here is a plunker showing all this in practice: http://plnkr.co/edit/kD5AmZyYEhJO0QQISgbM?p=preview
The above one is template ref value solution.
This is for ngModel solution.
Html code:
<input type="text" class="form-control" [resultTemplate]="resultTemplate" (selectItem)="onSelect($event)"
[(ngModel)]="model" placeholder="Start typing a customer name..." [ngbTypeahead]="search"/>
Component code:
onSelect($event) {
$event.preventDefault();
this.model = null;
this.router.navigate(['/customers', $event.item.resource.id]);
};
$event.preventDefault();
for ngModel value change empty
In a non Ember world I could put this in my document ready:
$("input").on("invalid", function(event) {
$(this).addClass('isDirty');
});
And then I would know that whenever a form is submitted, it would inturn fire the invalid event on invalid inputs and allow me to mark them as dirty for css purposes. I tried to do this in Ember in a component (in didInsertElement):
export default Ember.Component.extend({
didInsertElement: function() {
this.$('input, textarea').on('invalid', function() {
console.log('worked!');
Ember.$(this).addClass('isDirty');
});
},
actions: {
keyDownAction: function(value, event) {
// Mark input/textarea as dirty
Ember.run(() => {
this.$('input, textarea').addClass('isDirty');
})
if (this.get('keyDown')) {
this.sendAction('keyDown', value, event);
}
},
focusInAction: function(value, event) {
if (this.get('focusIn')) {
this.sendAction('focusIn', value, event);
}
},
focusOutAction: function(value, event) {
// Mark input/textarea as dirty
Ember.run(() => {
this.$('input, textarea').addClass('isDirty');
})
if (this.get('focusOut')) {
this.sendAction('focusOut', value, event);
}
}
}
})
hbs:
{{input type=type value=value id=inputId class=inputClass name=name tabindex=tabindex autofocus=autofocus required=required list=list
min=min max=max step=step
key-down="keyDownAction" focus-in="focusInAction" focus-out="focusOutAction"}}
<span class="bar"></span>
<label class="{{if value 'text-present'}}">{{placeholder}}</label>
But my event isn't firing. Can I attach the ember input helper to the html5 invalid event?
It works for me. You just need to make sure you wrap both component and submit button (for example, <button type='submit'>Submit</button>) in <form> element.
For example, template:
<form>
{{my-component type='text' required='true' placeholder='My field'}}
<button type='submit'>Submit</button>
</form>
Clicking submit button without <form> will do nothing. Clicking submit button when both elements are inside <form> will log worked! in console, add class isDirty to <input> and display native browser dialog to fill this field.
Working demo.
Full code behind demo.
It looks like you're doing things way too complicated. from what i understand is you're marking a textarea's validty depending on its value.
Lets say you have a text area
{{textarea value=textValue class="form-control"}}
And this input is supposed to have value for validity.
textAreIsValid: function() {
return !Ember.isEmpty(this.get('textValue'); // meaning the text can't be empty.
}.property('textValue')
To show its validity you to user, i would wrap the text area like this
<div class="control-group {{if textAreIsValid 'valid-class' 'has-error'}}">
XX text area XX
</div>
Also instead of keyPres and up you could be observing the value
areaValueChanged: function() {
}.observes('textValue'),
It turns out that adding an Ember action to the submit button, such as:
<button type="submit" {{action 'testAction'}}>Go</button>
And returning false from the action, was not enough.
I did try adding preventDefault=false to the button, which worked in that the invalid event fired, but didnt work in that the whole page then submited rather than Ember handling things
The best solution therefore was to call this.$('form')[0].checkValidity(); just before returning false in the action, i.e.
if (formIsInvalid) {
this.$('form')[0].checkValidity();
return false;
}
Example twiddle now working: https://ember-twiddle.com/13af9d78ff5007626960
I am trying to integrate zurb reveal with form into react component. So far next code properly displays modal form:
ModalForm = React.createClass({
handleSubmit: function(attrs) {
this.props.onSubmit(attrs);
return false;
},
render: function(){
return(
<div>
Add new
<div id="formModal" className="reveal-modal" data-reveal>
<h4>Add something new</h4>
<Form onSubmit={this.handleSubmit} />
<a className="close-reveal-modal">×</a>
</div>
</div>
);
}
});
The Form component is pretty standard:
Form = React.createClass({
handleSubmit: function() {
var body = this.refs.body.getDOMNode().value.trim();
if (!body) {
return false;
}
this.props.onSubmit({body: body});
this.refs.body.getDOMNode().value = '';
return false;
},
render: function(){
return(
<form onSubmit={this.handleSubmit}>
<textarea name="body" placeholder="Say something..." ref="body" />
<input type="submit" value="Send" className="button" />
</form>
);
}
});
Problem: When I render form component within modal form component and enter something into form input then I see in console exception Uncaught object. This is a stack:
Uncaught object
invariant
ReactMount.findComponentRoot
ReactMount.findReactNodeByID
getNode
...
If I just render form component directly in the parent component then everything works. Could anybody help please?
In short, you're doing this wrong and this is not a bug in react.
If you use any kind of plugin that modifies the react component's dom nodes then it's going to break things in one way or another.
What you should be doing instead is using react itself, and complementary css, to position the component in the way you'd like for your modal dialog.
I would suggest creating a component that uses react's statics component property to define a couple of functions wrapping renderComponent to give you a nice clean function call to show or hide a react dialog. Here's a cut down example of something I've used in the past. NB: It does use jQuery but you could replace the jQ with standard js api calls to things like elementById and etc if you don't want the jQuery code.
window.MyDialog = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
content: React.PropTypes.string.isRequired
},
statics: {
// open a dialog with props object as props
open: function(props) {
var $anchor = $('#dialog-anchor');
if (!$anchor.length) {
$anchor = $('<div></div>')
.prop('id', 'dialog-anchor');
.appendTo('body');
}
return React.renderComponent(
MyDialog(props),
$anchor.get(0)
);
},
// close a dialog
close: function() {
React.unmountComponentAtNode($('#dialog-anchor').get(0));
}
},
// when dialog opens, add a keyup event handler to body
componentDidMount: function() {
$('body').on('keyup.myDialog', this.globalKeyupHandler);
},
// when dialog closes, clean up the bound keyup event handler on body
componentWillUnmount: function() {
$('body').off('keyup.myDialog');
},
// handles keyup events on body
globalKeyupHandler: function(e) {
if (e.keyCode == 27) { // ESC key
// close the dialog
this.statics.close();
}
},
// Extremely basic dialog dom layout - use your own
render: function() {
<div className="dialog">
<div className="title-bar">
<div className="title">{this.props.title}</div>
<a href="#" className="close" onClick={this.closeHandler}>
</div>
</div>
<div className="content">
{this.props.content}
</div>
</div>
}
});
You then open a dialog by calling:
MyDialog.open({title: 'Dialog Title', content: 'My dialog content'});
And close it with
MyDialog.close()
The dialog always attaches to a new dom node directly under body with id 'dialog-anchor'. If you open a dialog when one is already open, it will simply update the dom based on new props (or not if they're the same).
Of course passing the content of the dialog as a props argument isn't particularly useful. I usually extend below to either parse markdown -> html for the content or get some html via an ajax request inside the component when supplying a url as a prop instead.
I know the above code isn't exactly what you were looking for but I don't think there's a good way to make a dom-modifying plugin work with react. You can never assume that the dom representation of the react component is static and therefore it can't be manipulated by a 3rd party plugin successfully. I honestly think if you want to use react in this way you should re-evaluate why you're using the framework.
That said, I think the code above is a great starting point for a dialog in which all manipulation occurs inside the component, which afterall is what reactjs is all about!
NB: code was written very quickly from memory and not actually tested in it's current form so sorry if there are some minor syntax errors or something.
Here is how to do what Mike did, but using a zf reveal modal:
var Dialog = React.createClass({
statics: {
open: function(){
this.$dialog = $('#my-dialog');
if (!this.$dialog.length) {
this.$dialog = $('<div id="my-dialog" class="reveal-modal" data-reveal role="dialog"></div>')
.appendTo('body');
}
this.$dialog.foundation('reveal', 'open');
return React.render(
<Dialog close={this.close.bind(this)}/>,
this.$dialog[0]
);
},
close: function(){
if(!this.$dialog || !this.$dialog.length) {
return;
}
React.unmountComponentAtNode(this.$dialog[0]);
this.$dialog.foundation('reveal', 'close');
},
},
render : function() {
return (
<div>
<h1>This gets rendered into the modal</h1>
<a href="#" className="button" onClick={this.props.close}>Close</a>
</div>
);
}
});