I'm struggling a bit with the proper pattern to use here. I have a model which represents a power selector called selector, each selector has a hasMany with selectorOption which makes up the options for the selector
I then have a dashboardItem model which loops over each selector and implements it.
route.js
export default Route.extend({
model(params) {
return RSVP.hash({
dashboard: get(this, 'store').findRecord('dashboard', params.dashboard_id),
selectors: get(this, 'store').findAll('selector'),
});
},
setupController(controller, models) {
controller.setProperties(models);
},
});
template.hbs
{{#each selectors as |selector|}}
<div class="column is-12 object-item">
<div class="card">
<header class="card-header">
<p class="card-header-title">
{{selector.title}}
</p>
</header>
<div class="card-content">
{{#power-select-multiple
placeholder="Vision"
options=selector.selectorOptions
searchEnabled=false
onchange=(action 'something...') as |option|}}
{{option.title}}
{{/power-select-multiple}}
</div>
</div>
</div>
{{/each}}
I'm not sure what to do on the onchange, either with a custom function or using built in tools of power-select.
Each selector is a multi-selector.
This works correctly to the point that I can create any number of selectors and they display on the front end with their correct options as expected.
How should I go about saving the options the users choose against the dashboardItem?
Here is a section from the database which shows the models and their relationships. Note there is currently no relationship between a selector and a dashboardItem (Maybe there should be though?)
{
"selectorOptions" : {
"-Kyc7on207d_IxnNw2iO" : {
"title" : "Apple",
"vision" : "-Kyc7nG9Bz3aEGLked8x"
},
"-Kyc7qC9_uxFgXP9c7hT" : {
"title" : "Orange",
"vision" : "-Kyc7nG9Bz3aEGLked8x"
},
"-Kyc7qqZPMikoG1r3r5g" : {
"title" : "Bannana",
"vision" : "-Kyc7nG9Bz3aEGLked8x"
},
"-Kyc7uZu8MTfUdH70cBR" : {
"title" : "Blue",
"vision" : "-Kyc7rtTPTMJxAPacg-L"
},
"-Kyc7vJC3ImzVOEraALx" : {
"title" : "Green",
"vision" : "-Kyc7rtTPTMJxAPacg-L"
},
"-Kyc7wCrqDz8CD_I-dYy" : {
"title" : "Red",
"vision" : "-Kyc7rtTPTMJxAPacg-L"
}
},
"selectors" : {
"-Kyc7nG9Bz3aEGLked8x" : {
"title" : "Fruits",
"selectorOptions" : {
"-Kyc7on207d_IxnNw2iO" : true,
"-Kyc7qC9_uxFgXP9c7hT" : true,
"-Kyc7qqZPMikoG1r3r5g" : true
}
},
"-Kyc7rtTPTMJxAPacg-L" : {
"title" : "Colours ",
"selectorOptions" : {
"-Kyc7uZu8MTfUdH70cBR" : true,
"-Kyc7vJC3ImzVOEraALx" : true,
"-Kyc7wCrqDz8CD_I-dYy" : true
}
}
}
}
The solution was to not fight against relationships with basic array storage.
For example
Base
export default Model.extend({
title: attr('string'),
visionOptions: hasMany('vision-option'),
});
Bases Options
export default Model.extend({
title: attr('string'),
vision: belongsTo('vision'),
});
The model to save the selected objects on
export default Model.extend({
//...
visionOptions: hasMany('vision-option', {async: true}),
//...
});
The component to handle saving, and selecting the correct objects
export default Component.extend({
tagName: "",
classNames: "",
selectedVisions: computed('dashboardItem.visionOptions', function () {
const visionId = this.get('vision.id');
const options = this.get('dashboardItem.visionOptions');
return options.filterBy('vision.id', visionId);
}),
actions: {
addVision(newList) {
let dashboardItem = get(this, 'dashboardItem');
let options = get(this, 'selectedVisions');
options.forEach(function (me) {
if (!newList.includes(me)) {
dashboardItem.get('visionOptions').removeObject(me);
}
});
newList.forEach(function (me) {
if (!options.includes(me)) {
dashboardItem.get('visionOptions').pushObject(me);
}
});
dashboardItem.save().then(() => {
dashboardItem.notifyPropertyChange('visionOptions')
});
}
}
});
Template to render power-select
{{#power-select-multiple
placeholder=""
options=vision.visionOptions
searchEnabled=false
selected=selectedVisions
onchange=(action 'addVision') as |vision|}}
{{vision.title}}
{{/power-select-multiple}}
This allows there to be an unknown number of "visions", with an unknown number of "visionObjects" to be loaded and saved.
The notifyPropertyChange is required to update the computed property so the frontend renders when a user adds or removes a selected object. This is only awkward because there isn't a direct known database key.
Related
The best example to illustrate what I am trying to develop is a desktop email application.
On the left there is a vertical menu (on a quasar q-drawer).
Next, also on the left, there is a mailing list (on a quasar q-list within a q-drawer).
When each item is selected, the corresponding content is displayed on the right (on a quasar q-page).
Expected operation:
The list is loaded once and when I successively select the various items in the list, only the content on the right should be used and the content updated according to the id sent as a parameter in the request.
Note that the list component is only rendered once; that is, it is not rendered again each time a item is selected from the list and remains visible while the content is displayed on the right
The problem:
When I select the first item in the mailing list it works correctly and as expected, the mail content is displayed on the q-page.
When I select a second item from the list it doesn't work anymore and the following error is displayed on the console:
Uncaught (in promise) NavigationDuplicated {_name:
"NavigationDuplicated", name: "NavigationDuplicated", message:
"Navigating to current location ("/mailcontent") is not allowed",
stack: "Error at new NavigationDuplicated
(webpack-int…node_modules/vue/dist/vue.runtime.esm.js:1853:26)"}
I would appreciate suggestions on how to resolve this issue.
The following code is intended to illustrate the problem in the main part:
Routes: secondlayout is the child of another layout
const routes = [
{
path: "/index",
component: () => import("layouts/AppLayout.vue"),
children: [
{ path: "/home", component: () => import("pages/Home.vue") },
{
path: "secondlayout",
component: () => import("Layouts/MailsPlace.vue"),
children: [
{ path: "/mailcontent", name: 'mailcontent', component: () => import("pages/MailContent.vue") },
]
}
]
}
];
Second layout where the email application (list and content) is rendered with q-drawer and router-view
<template>
<q-layout view="lhh LpR lff" container class=" myclass shadow-2 window-height" >
<q-drawer
style="full-height"
v-model="drawerLeft"
:width="500"
:breakpoint="700"
elevated
content-class="bg-grey-1"
>
<q-scroll-area
class="fit"
style="margin-top:80px">
<q-list separator padding>
<q-separator />
<list-mails
v-for="(mail, index) in mails"
:mail="mail"
:key="mail.id_mail"
:id="index">
</list-mails>
<q-separator />
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>
<script>
export default {
data () {
return {
mails: {},
drawerRight: false,
}
},
/* watch: {
$route(to, from) {
console.log('after', this.$route.path);
}
},
beforeRouteUpdate(to, from, next) {
console.log('before', this.$route.path);
next();
},*/
components: {
'list-mails': require("pages/ListMails.vue").default,
},
created: function() {
this.listMails()
},
methods: {
listMails(){
this.$axios.get("/listmails")
.then(response => {
if (response.data.success) {
this.mails = response.data.mails.data;
} else {
showErrorNotify('msg');
}
})
.catch(error => {
showErrorMessage(error.message);
});
}
}
</script>
Mail list item with mailitemclick method
<template>
<q-item
clickable
v-ripple
exact
#click="mailitemclick(mail.id_mail)"
>
<q-item-section>
<q-item-label side lines="2"> {{ mail.title_mail }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script>
export default {
props: ["mail"],
methods:{
mailitemclick(id){
this.$router.push({
name: 'mailcontent',
params: {id:id}
});
}
}
}
</script>
Mail content
<template>
<q-page class="fit row wrap justify-center tems-start content-start" style="overflow: hidden;">
<div style="padding:5px; margin:0px 0px 20px 0px; min-width: 650px; max-width: 700px;" >
<q-item>
<q-item-label class="titulo"> {{ mail.title_mail }} </q-item-label>
<div v-html="mail.content_mail"></div>
</q-item>
</div>
</q-page>
</template>
<script>
export default {
name: 'mailcontent',
data() {
return {
mail: {},
};
},
created() {
this.$axios.get(`/mailcontent/${this.$route.params.id}`)
.then(response => {
if (response.data.success) {
this.mail = response.data.mail[0])
} else {
showErrorNotify('msg');
}
})
.catch(error => {
showErrorMessage(error.message);
});
}
}
</script>
This happened to me when I had a router-link pointing to the same route. e.g. /products/1.
The user is able to click on the products, but if a product was
already clicked (and the component view was already loaded) and the
user attempts to click it again, the error/warning shows in the
console.
You can solve this by adding catch block.
methods: {
mailitemclick(id) {
this.$router.push({
name: 'mailcontent',
params: {'id': id}
}).catch(err => {});
}
},
But in the mail-content, you need to use watch for calling function and in mounted for first-time calling.
Temp Example -
data() {
return {
mail: {},
test_mails: {
12: {
content_mail: '<div>test 12<div>'
},
122:{
content_mail: '<div>test 122<div>'
}
}
}
},
mounted() {
this.mail = this.test_mails[this.$route.params.id]
},
watch:{
'$route':function () {
this.mail = this.test_mails[this.$route.params.id]
}
}
OR
You can use :to in list-mail to avoild click and catch -
<q-item
clickable
v-ripple
exact
:to="'/mailcontent/'+mail.id_mail"
>
<q-item-section>
<q-item-label side lines="2"> {{ mail.title_mail }}</q-item-label>
</q-item-section>
</q-item>
children: [
{ path: '', component: () => import('pages/Index.vue') },
{
path: "secondlayout",
component: () => import("layouts/mail-place.vue"),
children: [
{ path: "/mailcontent/:id", name: 'mailcontent', component: () => import("pages/mail-content.vue") },
]
}
]
I do not have the best understanding of how ember.js works. I am currently working on updating an attribute called features (an array of strings) that each owner has using a multi select checkbox. So far everything seems to be working except for the updated features attribute is not saving. When I click the checkbox it updates the selected computed property in the multi-select-checkboxes component. I thought if I was passing model.owner.features as selected to the component it would directly update the model when the component changes.
(function() {
const { shroud } = Sw.Lib.Decorators;
Sw.FranchisorNewAnalyticsConnectionsUsersRoute = Ember.Route.extend({
currentFranchisorService: Ember.inject.service('currentFranchisor'),
#shroud
model({ account_id }) {
console.log("load model in route")
return Ember.RSVP.hash({
account: this.get('store').find('account', account_id),
owners: Sw.AccountOwner.fetch(account_id),
newAccountOwner: Sw.AccountOwner.NewAccountOwner.create({ accountID: account_id }),
});
},
actions: {
#shroud
accountOwnersChanged() {
this.refresh();
},
close() {
this.transitionTo('franchisor.newAnalytics.connections');
},
},
});
})();
users controller:
(function() {
const { shroud, on, observer } = Sw.Lib.Decorators;
Sw.FranchisorNewAnalyticsConnectionsUsersController = Ember.Controller.extend(Sw.FranchisorControllerMixin, {
isAddingUser: false,
adminOptions: [{
label: 'Employee Advocacy',
value: 'employee advocacy'
}, {
label: 'Second Feature',
value: 'other'
}, {
label: 'Can edit users?',
value: 'edit_users'
}],
});
})();
users.handlebars
{{#each model.owners as |owner|}}
<tr
<td>
{{owner.name}}
</td>
<td>
{{owner.email}}
</td>
<td>{{if owner.confirmedAt 'Yes' 'No'}}</td>
<td>
{{log 'owner.features' owner.features}}
{{multi-select-checkboxes
options=adminOptions
selected=owner.features
owner=owner
model=model
}}
</td>
<td>
<button class="btn light-red-button"
{{action "remove" owner}}>
Remove
</button>
</td>
</tr>
{{/each}}
multi-select-checkboxes.handlebar:
{{#each checkboxes as |checkbox|}}
<p>
<label>
{{input type='checkbox' checked=checkbox.isChecked}}
{{checkbox.label}}
</label>
</p>
{{/each}}
multi_select_checkboxes.jsx:
// Each available option becomes an instance of a "MultiSelectCheckbox" object.
var MultiSelectCheckbox = Ember.Object.extend({
label: 'label',
value: 'value',
isChecked: false,
changeValue: function () { },
onIsCheckedChanged: Ember.observer('isChecked', function () {
var fn = (this.get('isChecked') === true) ? 'pushObject' : 'removeObject';
this.get('changeValue').call(this, fn, this.get('value'));
})
});
Sw.MultiSelectCheckboxesComponent = Ember.Component.extend({
labelProperty: 'label',
valueProperty: 'value',
// The list of available options.
options: [],
// The collection of selected options. This should be a property on
// a model. It should be a simple array of strings.
selected: [],
owner: null,
model: null,
checkboxes: Ember.computed('options', function () {
console.log("CHECKBOXES", this.get('model'))
var _this = this;
var labelProperty = this.get('labelProperty');
var valueProperty = this.get('valueProperty');
var selected = this.get('selected');
var model = this.get('model');
return this.get('options').map(function (opt) {
var label = opt[labelProperty];
var value = opt[valueProperty];
var isChecked = selected.contains(value);
return MultiSelectCheckbox.create({
label: label,
value: value,
isChecked: isChecked,
model: model,
changeValue: function (fn, value, model) {
_this.get('selected')[fn](value);
console.log("model in middle", this.get('model'))
this.get('model').save();
}
});
});
}),
actions: {
amountChanged() {
const model = this.get(this, 'model');
this.sendAction('amountChanged', model);
}
}
});
Seems like you have a pretty decent understanding to me! I think your implementation is just missing a thing here or there and might be a tad more complex than it has to be.
Here's a Twiddle that does what you're asking for. I named the property on the model attrs to avoid possible confusion as attributes comes into play with some model methods such as rollbackAttributes() or changedAttributes().
Some things to note:
You don't need to specify a changeValue function when creating your list of checkbox objects. The onIsCheckedChanged observer should be responsible for updating the model's attribute. Just pass the model or its attribute you want to update (the array of strings) into each checkbox during the mapping of the options in the multi select checkbox:
return Checkbox.create({
label: option.label,
value: option.value,
attributes: this.get('owner.attrs') // this array will be updated by the Checkbox
});
If the model you retrieve doesn't have any data in this array, Ember Data will leave the attribute as undefined so doing an update directly on the array will cause an error (e.g., cannot read property 'pushObject' of undefined) so be sure the property is set to an array before this component lets a user update the value.
The observer will fire synchronously so it might be worthwhile to wrap it in a Ember.run.once() (not sure what else you will be doing with this component / model so I note this for completeness).
If you want to save the changes on the model automatically you'll need to call .save() on the model after making the update. In this case I would pass the model in to each checkbox and call save after making the change.
I currently have a route servicerequests.index. In this route I am displaying a table of all service requests that I am getting via this call in my route:
model() {
return this.store.findAll('servicerequest').then(results =>
results.sortBy(this.get('selectedFilter')).reverse());
}
In my hbs file I have this power select:
{{#power-select options=filterOptions placeholder="Select a filter"
selected=selectedFilter onchange=(action (mut selectedFilter))
searchField="filterOptions" as |filter|}}
{{filter.descr}}
{{/power-select}}
In my controller I am defining the options:
filterOptions: [
{ descr: "created_at" },
{ descr: "priority" },
{ descr: "status" }],
actions: {
selectedFilter(filter) {
this.set('selectedFilter', filter);
}
},
What I want to happen is that when I select the different filter sort options to reorder the results on the page. How do you go about doing that?
With the data coming from the route and the options set in the controller I wasn't sure where to do the logic of the power selected.
Any thoughts or suggestions are appreciated.
Use a computer property in the controller for the sorting. Don't do it in the model hook.
My answer here is very related to this.
basically do this in your route:
model() {
return this.store.findAll('servicerequest');
}
and then in your controller:
filtetedModel: computed('selectedFilter', 'model.#each.{created_at,priority,status}', {
get() {
return this.model.sortBy(this.selectedFilter);
}
}),
filterOptions: [
{ descr: "created_at" },
{ descr: "priority" },
{ descr: "status" }
],
actions: {
selectedFilter(filter) {
this.set('selectedFilter', filter);
},
}
I have a router that returns data from the data model.
I want to use this data to bind it to a widget in a view.
Model:
myApp.Unit = DS.Model.extend({
active: DS.attr('boolean'),
allowdecimals: DS.attr('boolean'),
name: DS.attr('string'),
count: DS.attr('number'),
});
Router:
myApp.EunitsRoute = Ember.Route.extend({
model: function() {
return this.store.find('unit');
},
setupController: function(controller, model) {
this._super(controller, model);
controller.set('units', model);
},
actions: { ...
In the view I expect an object formated as follows:
[ {"id": 1,"active": true,"allowdecimals": true,"name": "Pint","count": 8},
{"id": 2,"active": true,"allowdecimals": true,"name": "Each","count": 8},
...]
What I am getting now in the view is an object:<DS.RecordArray:ember400>
View:
var source10 = {
datatype: "array",
datafields: [
{ name: 'id' },
{ name: 'name' },
{ name: 'allowdecimals' },
{ name: 'active' },
{ name: 'count' }
],
localdata: controller.get('units')
};
var dataAdapter10 = new $.jqx.dataAdapter(source10);
$("#eunits_grid").jqxGrid({
pageable: true,
theme: 'energyblue',
autorowheight : true,
rowsheight : 50,
pagesize: 10,
source: dataAdapter10,
....
Template:
<script type="text/x-handlebars" data-template-name="eunits">
<div class="col-md-12">
<h3 class="page-header">Edit Units</h3>
{{#if adding}}
{{view AddnewunitView}}
{{/if}}
{{view UnitsView id='eunits_grid'}}
</div>
</script>
The intent of my code is to be able to load various fixtures into a template. For example:
The family template:
{{#each model as family}}
<p>{{family.name}} + "is" + {{family.age}} + "years old"</p>
{{/each}}
The brothers fixture: [{ id:1, name:"Greg", age:40 }]
The sisters fixture: [{ id:1, name:"Mary", age:35 }]
So that when I call:
#/brothers I'd get <p>Greg is 40 years old</p>
vs
#/sisters I'd get <p>Mary is 35 years old</p>
I figure I'm having trouble with 1) the correct routing. 2) usage of {{each}}/{{with}}. 3) usage of fixtures/models. You can find EVERYTHING at my github repo. Thanks in advance for the help!
Application Template - application.hbs:
<br><br>
<div class="container">
<ul class="nav nav-pills" role="tablist">
<li class="active">Home</li>
<li>Brothers</li>
<li>Sisters</li>
</ul>
<div class="container">
{{outlet}}
</div>
</div>
Family template (to go into {{outlet}} of application) - family.hbs:
<h1>Fixtures and Models</h1>
{{#each in model as family}}
<p>{{family.name}} is here</p>
{{else}}
<li>DIDN'T WORK</li>
{{/each}}
Family models and fixtures - family.js:
App.Family = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Brothers = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Sisters = DS.Model.extend({
name : DS.attr(),
age : DS.attr()
});
App.Brothers.FIXTURES = [
{
"id" : 1,
"name" : "Greg",
"age" : 10
},
{
"id" : 2,
"name" : "Mike",
"age" : 23
}
];
App.Sisters.FIXTURES =
[
{
"id" :1,
"name" :"Mary",
"age" :15
},
{
"id" :2,
"name" :"Gina",
"age" :34
}
];
My app.js file for all the routes:
App = Ember.Application.create({
LOG_TRANSITIONS: true
});
App.Router.map(function() {
this.resource('application');
this.resource('home');
this.resource('brothers');
this.resource('sisters');
});
App.IndexRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('application');
}
});
App.FamilyRouter = Ember.Route.extend({
});
App.HomeRoute = Ember.Route.extend({
});
App.FamilyRoute = Ember.Route.extend({
});
App.BrothersRoute = Ember.Route.extend({
model: function() {
this.store.find('brothers');
},
renderTemplate: function() {
return this.render('family');
}
});
App.SistersRoute = Ember.Route.extend({
model: function() {
return this.store.find('sisters');
}
});
Again, you can find all the codez here at my github repo
You need two fixes.
First, you're using wrong #each syntax. Change it to this:
<h1>Fixtures and Models</h1>
{{#each family in model}}
<p>{{family.name}} is here</p>
{{else}}
<li>DIDN'T WORK</li>
{{/each}}
Second, you're not returning brothers models. Change to:
App.BrothersRoute = Ember.Route.extend({
model: function() {
return this.store.find('brothers');
},
templateName: 'family'
});