How to Create a Dropdown Menu with Static Items in Ember - javascript

I'm very new to JS/Ember and have been able to create a static dropdown menu that functions as desired however I've duplicated the static array of objects that holds the dropdown items in my model and controller which violates DRY.
There are two routes (index and requests/new) that need to access this static array object (reasonList) that holds the dropdown items - one has a model and controller (requests) and the other (index) only has a controller. My understanding is that controllers can read from models but not vice versa so it seems like the model is the correct place for this static array object. I've tried many different ways to resolve this but none have succeeded so instead I'm just asking what the right way to do this is.
ember-cli version: 3.15.1
node version: 10.18.0
ember-source version: 3.3.2
models/request.js:
import { belongsTo } from 'ember-data/relationships';
import Model from 'ember-data/model';
import DS from 'ember-data';
export default Model.extend({
// Properties
reason_bucket: DS.attr('string'),
reason: DS.attr('string'),
approved_at: DS.attr('number'),
reason_label: computed('reason_bucket', function() {
let reasonList = [
{ value: "testDebug", label: "Test & Debug" },
{ value: "auditSupport", label: "Audit Support" },
{ value: "incidentResponse", label: "Incident Response / Case Support" }
]
return reasonList.findBy('value', get(this, 'reason_bucket')).label;
}), // snip
controllers/requests/new.js:
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
import {computed, get, set} from '#ember/object';
import Controller from '#ember/controller';
import SearchAccountsMixin from '../../mixins/search-accounts';
export default Controller.extend(SearchAccountsMixin, {
init() {
this._super(...arguments);
this.clear();
this.reasonList = [
{ value: "testDebug", label: "Test & Debug" },
{ value: "auditSupport", label: "Audit Support" },
{ value: "incidentResponse", label: "Incident Response / Case Support" },
];
},
clear() {
set(this, 'reason_bucket', null);
},
selectedReason: computed('reason_bucket', function(){
return this.get('reasonList').findBy('value', this.get('reason_bucket'));
}),
actions: {
setSelectedReason(selectedVal){
this.set('reason_bucket', selectedVal.value);
get(this, 'changeset').set('reason_bucket', selectedVal.value);
}, // snip
submit(changeset) {
changeset.validate().then(() => {
if (changeset.get('isValid')) {
changeset.save().then(() => {
this.transitionToRoute('index')
})
}
})
}
},
// Computed properties
changeset: computed('model', function () {
return new Changeset(get(this, 'model').request, lookupValidator(AccessRequestValidator), AccessRequestValidator, {skipValidate: true});
}), // snip
templates/requests/new.hbs:
<div class="omitted">
{{#power-select
options=reasonList
searchField="label"
placeholder="Select a Reason for Access"
selected=selectedReason
onchange=(action 'setSelectedReason')
as |reason_bucket|
}}
{{reason_bucket.label}}
{{/power-select}}
{{group.errorsList}}
</div>
templates/index.hbs:
<div>
<div class="b">Reason </div>
<div>{{request.reason_label}}</div>
</div>
<div>
<div class="b">Reason Detail </div>
<div>{{request.reason}}</div>
</div>

If what you’re wanting to do is DRY out your list and find a good home for it, I’d suggest putting your array (and possibly search functionality in a utils file - which doesn’t exist but which you can create and import from). Then you’d be able to do something like:
// app/utils/reasonList.js
export const reasonList = [];
import { reasonList } from ‘appName/utils/reasonList’;
selectedReason: computed('reason_bucket', function(){
return reasonList.findBy('value', this.get('reason_bucket'));
}),

Related

how to v-model on different array vue 3

Hi I was trying to use a v-model an input to a value in object in an array of object in Vue 3. The complexity lies in the fact the object is first processed by a function. And that it need to be processed every time when a change is made to an input. Here is my code (and a sandbox link) :
<template>
<div id="app">
<div v-for="param in process(parameters)" :key="param">
Name : {{param.name}} Value : <input v-model="param.value">
</div>
{{parameters}}
</div>
</template>
<script>
export default {
name: "App",
data(){
return{
parameters :[
{'name':'Richard Stallman','value':'cool dude'},
{'name':'Linus Torvalds','value':'very cool dude'},
{'name':'Dennis Ritchie','value':'very very cool dude'}
]
}
},
methods:{
process(parameters){
const results = parameters.map( param =>{
return {name:param.name+' extra text',
value:param.value+' extra text',
}
})
return results
}
}
};
</script>
I just want the original parameters to change when something is types in the inputs. Maybe #change could be of use. But I didn't find a fix with #change. Does anyone know a solution to my problem? Thanks in advance.
Use computed property to get reactive state of the data.
Working Demo :
new Vue({
el: '#app',
data: {
parameters :[
{'name':'Richard Stallman','value':'cool dude'},
{'name':'Linus Torvalds','value':'very cool dude'},
{'name':'Dennis Ritchie','value':'very very cool dude'}
]
},
computed: {
process() {
const results = this.parameters.map((param) => {
return {
name: param.name + ' extra text',
value: param.value + ' extra text'
}
});
return results;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="param in process" :key="param">
Name : {{param.name}}
Value : <input v-model="param.value">
</div><br>
<strong>Orinigal Data :</strong> {{parameters}}
</div>
I am not entirely sure I understood whether the person should be able to see/edit the text you added within you processing method.
Anyway, I think this sample of code should solve you problem :
<template>
<div id="app">
<div v-for="param in parameters" :key="param.name">
Name : {{ param.name }} Value : <input v-model="param.value" />
</div>
{{ process }}
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
parameters: [
{ name: "Richard Stallman", value: "cool dude" },
{ name: "Linus Torvalds", value: "very cool dude" },
{ name: "Dennis Ritchie", value: "very very cool dude" },
],
};
},
computed: {
process: function() {
const results = this.parameters.map((param) => {
return {
name: param.name + " extra text",
value: param.value + " extra text",
};
});
return results;
},
},
};
</script>
So, we're iterating through the parameters array directly, adding an input on the value just like you did.
When you type in the input, you update the parameter linked to it, in live.
I just switched the method you made into a computed method.
This way, every time parameters is updated, "process" is also updated because it's depending on it directly.
I also removed passing the "parameters" argument, it's in the component data, you can just access it directly.
This way, using "process" just like any variable, you'll always have the updated parameters + what you added to em.

Input Binding in Ember

I have a form and a live preview of what the form will create.
My model
//campaign.js
export default Model.extend({
title: attr('string'),
body: attr('string')
});
In the route
// new.js
export default Ember.Route.extend({
model () {
return this.store.createRecord('campaign', {
title: null,
body: null
})
}
});
My current implementation uses a component for the input
export default Ember.Component.extend({
keyPress(event) {
// binding code
}
});
And in the template
{{#title-input}}
{{/title-input}}
<div id="title-preview"></div>
My Feeling is that there is a cleaner or more idiomatic way to do this. I am new to ember so thank you for any help
While the use of Components are compelling they aren't required for capturing form input in ember. For what what its worth. For simple form input the route could be:
setupController() {
Ember.set('controller','newCampaign', {}); //set empty newCampaign
},
# Action hash would create the new record but only when you have some data.
actions: {
createCampaign(newCampaign) {
let newRecord = this.store.createRecord('campaign', newCampaign); //create record
newRecord.save().then(( /* response */ ) => {
this.transitionTo('campaigns'); //transition to different page.
}, (error) => { // Deal with an adapter error
//handle error
//rollback if necessary
});
}
}
The form or template could be:
{{input name="title" id="title" value=newCampaign.title type="text"}}
{{input name="body" id="body" value=newCampaign.body type="text"}}
Just a suggestion.
Jeff

Mongo: Get data of two collections - second depending on first one

I try to display an article and below of that article I want to display some literature data which belongs to the article.
In the collection literature there are many documents, but I want to filter those which has article_id: article._id
I believe I think to complicated, but this is what I'm trying to do:
publication.js
Meteor.publish('references', function(){
return Articles.find({});
});
Router.js
Router.route('/cars', {
name: 'main',
data: function() {
return {
article: Articles.find({})
}
}
});
template_1.html
<template name="main">
<div>
{{article._id}}
<header><h1>{{article.title}}</h1></header>
{{article.content}}
{{>literature reference=article._id}}
</div>
</template>
template_2.html
<template name="literature">
Same id: {{this.reference}}
Now get all item of literature-collection
<ul>
{{#each items}}
<li>{{this.title}}</li>
{{/each}}
</ul>
</template>
helper.js
Template.literature.helpers({
items: function() {
return Literature.find({article_id: article._id}); /* this should be the id the first template */
}
});
Guess that could be easier. So my second think was to put the literature-query also into the router:
Router.route('/cars', {
name: 'main',
data: function() {
return {
article: Articles.find({}),
items: Literature.find({article_id: article._id}) /* How do I get the variable which is needed? */
}
}
});
You should use meteor add reywood:publish-composite
so your publish function would like this:
Meteor.publishComposite('articles', function(){
return {
find: function(){
return Articles.find();
},
children: [{
find: function(article){
return Literature.find({article_id: article._id})
}
}]
}
});

Ember.js show hasMany data in template with one or more results

After endless trying I hope someone find the clue in what I am trying. I know there are many questions about this specific topic on stackoverflow. However I think I do not ask the same question. As I do not find the answer to my specific challenge.
Here is my Router:
App.Router.map(function () {
this.resource('article', {path: '/article/:id'});
this.resource('article.new', {path: "/article/new"});
});
Routes
App.ArticleRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('article', params.id);
}
});
App.ArticleNewRoute = Ember.Route.extend({
renderTemplate: function () {
this.render('article', {
controller: 'article.new'
});
},
model: function () {
return this.store.createRecord('article');
}
});
The model
App.Category = DS.Model.extend({
name: DS.attr('string'),
image: DS.attr('string'),
categoryRelation: DS.belongsTo('category')
});
App.Article = DS.Model.extend({
name: DS.attr('string'),
category: DS.hasMany('category')
)};
The returned JSON from server:
{
"articles":[
{
"id":1,
"name":"Car 1",
"category":[1,2],
{
"id":2,
"name":"Car 2",
"category":2,
],
"categorys":[ // note the added 's' when returning multiple items as per EmberJS convention
{
"id":1,
"name":"Oldtimers"
},
{
"id":2,
"name":"Classic"
}
],
}
And now the question, because I try in my template the following:
<script type="text/x-handlebars" data-template-name="article">
<div>
{{#each category in model}}
{{category.name}}<br>
{{name}}<br>
{{/each}}
</div>
</script>
I have tried multiple variations in the template, this is my last code which seems correct. Note: as for article with id 2, the template must also render if there is just one article.
Edit: I translated some code for you guys. If there are misspellings, they are probably not in the original code.
Your article template will receive just one article so this {{#each category in model}} don't work, you need to use {{#each category in model.category}}:
<div>
Article {{name}}<br/>
{{#each category in model.category}}
Category {{category.name}}<br/>
{{/each}}
</div>
This is a fiddle with this in action http://jsfiddle.net/marciojunior/fj26R/

Ember bindings firing when not wanted, and not firing when required

Hey I'm having two different issues in my ember app, both of which involve bindings.
First, I have a binding firing when I don't want it to. Basically what I'm trying to achieve (I'm building a survey creator front-end app) is that when any text is entered into the 'name' field of a question, I want to add a new question object, which will render out another blank question at the end of the list of questions that the user is adding. This has the effect of there always being a new question, so an add question button is not required. The binding is working, and a new object is being added: however, since the binding is from the newest question object, the binding is triggered again when the new object is created, which in turn creates a new object, which triggers the binding again....which obviously eventually crashes the browser. I've tried using the Ember._suspendObserver function, but there isn't a lot of documentation on this, and I think I'm using it wrong - anyhow it isn't suspending the observer or pausing the binding. The observer in the code is around line 27 (contentsNameObserver)
The other issue I'm having -- I have a selection drop down box which selects what type of question the user wants (single answer, multi-choice, etc.) but the binding between the select box and the {{#each}} helper which renders the kind of question isn't triggering. I'm using the Ember.Select view helper, so there shouldn't be any issues with using get/set to fire the binding. I'm using a computed property to return an array of fields for the question type based on the value of the question type id. The computed property is in line 13 (App.SurveyContent.types), and the template templates/step3. Quick heads up that this app may be extended for more than surveys, hence 'questions' are often referred to in the code as 'content'.
I'm pretty new to ember (this is my first real app) so my code most likely has a lot of issues outside of these problems...so any comments on how I've structured my app would be hugely appreciated as well!
Javascript ember app:
App = Ember.Application.create({
rootElement: '#emberContainer'
});
App.SurveyContent = Ember.Object.extend({
name: "",
content_type: 1,
content_pos: 1,
hash: Em.A([]),
types: function() {
alert("redraw");
return App.ContentTypes[this.content_type-1].hash;
}.property()
});
App.Surveys = Ember.Object.create({
name: null,
start: $.datepicker.formatDate('mm/dd/yy' , new Date()),
end: $.datepicker.formatDate('mm/dd/yy' , new Date()),
themeID: 0,
contents: [App.SurveyContent.create()], //Pushing an instance of App.SurveyContent onto this
contentsNameObserver: function() {
context = this;
console.log("entering");
Em._suspendObserver(App.Surveys, "contents.lastObject.name", false, false, function() {
console.log("suspend handler");
context.contents.pushObject(App.SurveyContent.create());
})
}.observes("contents.lastObject.name")
});
App.ContentTypes = [
Ember.Object.create({name: 'Text question', id:1, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'})]}),
Ember.Object.create({name: 'Multichoice question', id:2, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'}),
Ember.Object.create({name: 'Answer', help: 'Enter possible answers here', type: 'text', multiple: true})]})
];
App.ViewTypeConvention = Ember.Mixin.create({
viewType: function() {
console.log(this);
return Em.get("Ember.TextField");
}.property().cacheable()
});
App.CRMData = Ember.Object.extend();
App.CRMData.reopenClass ({
crm_data: [],
org_data: [],
org_display_data: [],
loadData: function() {
context = this;
context.crm_data = [];
$.getJSON ("ajax/crm_data", function(data) {
data.forEach(function(crm) {
context.crm_data.pushObject(App.CRMData.create({id: crm.crm_id, name: crm.crm_name}));
crm.orgs.forEach(function(org) {
context.org_data.pushObject(App.CRMData.create({id: org.org_id, name: org.org_name, crm_id: crm.crm_id}));
}, context)
}, context)
context.updateOrganisations(5);
});
return this.crm_data;
},
updateOrganisations: function(crm_id) {
context = this;
this.org_display_data.clear();
console.log("clearing the buffer")
console.log(this.org_display_data)
context.org_data.forEach(function(org) {
if(org.crm_id == crm_id) {
context.org_display_data.pushObject(App.CRMData.create({id: org.id, name: org.name}));
}
}, context)
}
});
App.DateField = Ember.TextField.extend({
attributeBindings: ['id', 'class']
});
App.CRMSelect = Ember.Select.extend({
attributeBindings: ['id'],
change: function(evt) {
console.log(evt)
App.CRMData.updateOrganisations($('#crm').val())
}
});
App.ApplicationController = Ember.Controller.extend();
App.Step1Controller = Ember.ArrayController.extend({});
App.Step2Controller = Ember.ArrayController.extend({});
App.Step2Controller = Ember.ArrayController.extend({});
App.ApplicationView = Ember.View.extend({
templateName: 'app'
});
App.Step0View = Ember.View.extend ({
templateName: 'templates/step0'
});
App.Step1View = Ember.View.extend ({
templateName: 'templates/step1'
});
App.Step2View = Ember.View.extend ({
templateName: 'templates/step2',
didInsertElement: function() {
$( ".jquery-ui-datepicker" ).datepicker();
}
});
App.Step3View = Ember.View.extend ({
templateName: 'templates/step3',
});
App.Router = Em.Router.extend ({
enableLogging: true,
root: Em.Route.extend ({
showstep1: Ember.Route.transitionTo('step1'),
showstep2: Ember.Route.transitionTo('step2'),
showstep3: Ember.Route.transitionTo('step3'),
index: Ember.Route.extend({
route: '/',
connectOutlets: function(router){
router.get('applicationController').connectOutlet( 'step0');
}
}),
step1: Ember.Route.extend ({
route: 'step1',
connectOutlets: function(router){
router.get('applicationController').connectOutlet( 'step1', App.CRMData.loadData());
}
}),
step2: Ember.Route.extend ({
route: 'step2',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('step2')
},
}),
step3: Ember.Route.extend ({
route: 'step3',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('step3')
},
})
})
});
Ember.LOG_BINDINGS=true;
App.LOG_BINDINGS = true;
App.ContentTypes.forEach(function(object) {
object.hash.forEach(function(hash) {
hash.reopen(App.ViewTypeConvention);
}, this);
}, this);
Html templates (I've got these in haml, so this is just a representation of the important ones)
<script type="text/x-handlebars" data-template-name="app">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="templates/step3">
<h1> Add content to {{App.Surveys.name}} </h1>
<br>
<div id = "accordion2" class = "accordion">
{{#each content in App.Surveys.contents}}
<div class="accordion-group">
<div class = "accordion-heading">
<a class = "accordion-toggle" data-parent = "#accordion2" data-toggle = "collapse" href = "#collapseOne">
{{content.name}}
</a>
</div>
<div id = "collapseOne" class = "accordion-body collapse in">
{{view Ember.TextField valueBinding="content.name" class="txtName"}}
<form class = "form-horizontal">
<div class = "accordion-inner">
<div class = "control-group">
<label class = "control-label" for ="organisation">
Content Type
<div class = "controls">
{{view Ember.Select contentBinding="App.ContentTypes" optionValuePath="content.id" optionLabelPath="content.name" valueBinding="content.content_type"}}
</div>
</div>
</div>
{{#each item in content.types }}
<div class = "control-group" >
<label class = "control-label" for = "organisation">
{{item.name}}
<div class = "controls">
{{view item.viewType }}
</div>
{{/each}}
</div>
</form>
</div>
{{/each}}
</div>
</div>
<br>
<div class = "btn" {:_action => 'showstep3'}> Next Step > </div>
</script>
I've solved the first issue, although I didn't get the suspendObserver property working I used an if statement to check the previous element, removing the infinite loop.
contentsNameObserver: function() {
context = this;
if(this.get('contents.lastObject').name) {
context.contents.pushObject(App.SurveyContent.create());
}
}.observes("contents.lastObject.name")
Any comments on how to get the _suspendObserver handler working would be appreciated though, it is something that should work but I'm doing something wrong
I've created a stripped down jsfiddle at http://jsfiddle.net/reubenposthuma/sHPv4/
It is set up to go straight to the problem step, step 3, so that I don't need to include all the previous templates.
I'm still stuck on the issue of the binding not firing though. The behaviour I'm expecting is that when the 'Content Type' dropdown box is changed, the text box underneath should change, it should re-render with two text boxes.
I realise this is an old question, but there is no documenation and precious little information I could find searching either, hence sharing what I found worked here.
What I found worked was to call Ember._suspendObserver as follows:
somePropertyDidChange: function(key) {
var that = this;
Ember._suspendObserver(this, key, null,
'somePropertyDidChange', function() {
// do stuff which would normally cause feedback loops
that.set('some.property', 'immune to feedback');
});
}.observes('some.property');
You can also use the multiple observer variant as follows:
somePropertiesDidChange: function(key) {
var that = this;
Ember._suspendObservers(this, ['some.property', 'another.property'],
null, 'somePropertiesDidChange', function() {
// do stuff which would normally cause feedback loops
that.set('some.property', 'immune to feedback');
that.set('another.property', 'also immune to feedback');
});
}.observes('some.property', 'another.property');
In my exact use case I actually called Ember._suspendObservers from an Ember.run.once() function which was setup by the observer since I wanted to make sure a number of dependant properties had settled before doing calculations which in turn would mutate some of those properties.

Categories