How to post many-to-many relational data to a restful api from AngularJS? - angularjs

I have a Theme Management module in my web application. I'รถ using SequelizeJS in server side.
Models are:
module.exports = function(sequelize, DataTypes) {
var Theme = sequelize.define('Theme', {
name: DataTypes.STRING,
description: DataTypes.STRING
}, {
associate: function(models) {
Theme.belongsToMany(models.Option, { through: models.ThemeOptions })
},
tableName: 'themes'
});
return Theme;
};
module.exports = function(sequelize, DataTypes) {
var ThemeOptions = sequelize.define('ThemeOptions', {
}, {
tableName: 'theme_options'
});
return ThemeOptions;
};
module.exports = function(sequelize, DataTypes) {
var Option = sequelize.define('Option', {
key: DataTypes.STRING,
value: DataTypes.STRING
}, {
associate: function(models) {
Option.belongsToMany(models.Theme, { through: models.ThemeOptions })
},
tableName: 'options',
timestamps: false
});
return Option;
};
In /#/themes/create state, I want to create a theme with some options, like color codes.
I am creating a theme with
$http.post('/themes', themeData)
then with it's it, I am creating options. Finally I should post many-to-many data to theme options. So for a theme that has 10 options, I am posting 21 times.
What is the best way to post many-to-many data to a REST server?

Don't really know how you handle your routing or backend. Supposing you're using express and your options are previously created, I'd recommend creating a new post route for handling each ThemeOptions
app.post('/themeOptions', { ThemeId: 1, OptionId:2 });
and use that info to create a ThemeOptions instance to join a Theme with certain Option.
This could reduce your post quantity to the half + 1 (one for the Theme and one for each ThemeOption).
Another solution is to maybe manage an array of ThemeOptions and use ThemeOptions#bulkCreate to create them at once, using only 2 posts (one for the Theme and one for all the ThemeOptions.
Would be something like this:
app.post('/themeOptions', {
options: [{
ThemeId: 1,
OptionId:2
}, {
ThemeId: 1,
OptionId:3
}
// and so on...
});
Each of these solutions could involve more logic to manage each front end request, but could increase front end behavior as well.
A final (and more complex at the backend) solution would be to send a unique post sending both, the Theme and the Options array, and create all the ThemeOptions after creating the Theme
// frontend
app.post('/theme', {
theme: {
name: 'John',
description: 'Doe'
},
options: [2, 3 /* and so on ... */]
});
// backend
Theme.create(req.body.theme).on('success', function (theme) {
var options = req.body.options.map(function (option) {
return {
ThemeId: theme.id,
OptionId: option
};
});
ThemeOptions.bulkCreate(options);
})

In the Symfony2 world, there is a bundle, SonataAdminBundle, that generates admin interfaces. With the entity classes (here, Option and Theme), it generates all the pages, listing, creating and editing theses entities. It generates forms that handles many to many relationship. Here how it manages that :
The user consults the creation/edition form of the entity of any side of the ManyToMany relation. In the form, where it have to display the many to many association, it displays a <select>, with Select2 for instance, which is a <select> with some jQuery. Each element of the list is linked with the corresponding ID in the database, something like <option value="13456">Option #3</select>. For a many to many relationship, we can select multiple fields at the time. Internally, it builds an array of Option IDs with the <select>.
If we want to add a inverse entity on the fly (here, the inverse entity is Option, I think ...), there is a button that open ups an Option creation form, and once the new Option is added, it adds the newly created option in the <select>, so the user can add it in the form immediately.
Then, it sends the array of Option ID's built with the form.
I think this strategy could fit your needs.

Related

.push() is creating duplicate table filter dropdowns when page is revisited

I have custom table with a filters property. 3 of the filters are hard coded, but 1 of the filters is gathered through a POST request from our API because their filter options are frequently updated in our database by non-engineering folks.
My issue is that when you hit the back button to get to the page with this custom table, a duplicate Gateways filters is added to the page. This item disappears when you refresh the page, which demonstrated to me that I am running into an issue with the State. How can I make it so the Gateways filter is only displayed once?
Here is the code for the table:
<CredcapTable
defaultSort="created_at"
cols={cols}
filters={filters}
searchFunc={this.searchFunc}
recordMappingFunc={this.recordMappingFunc}
/>
Here is the function that pulls the Gateways filters from the database, which is called in componentDidMount:
private loadFilters = () => {
searchGateways(this.props.app.api, new SearchRequest()).then((res: SearchResults<Gateway>) => {
const opts = new Array<any>();
opts.push({ label: "All", value: "*" });
res.results.map((g: Gateway) => {
opts.push({ label: `${g.name} [${g.code}]`, value: g.id });
});
filters.push({
field: "gateway_id",
label: "Gateway",
options: opts
});
this.setState({ filters });
});
};

Best way to layout application?

Edit: Fixed JSFiddle Link
So i've been playing with Backbone and Marionette since a couple of weeks. I did some courses on Udemy, read the docs for both Backbone and Marionette. I can grasp most of the logic but somehow my mind can't wrap itself around the best way to approach a SPA I am trying to create.
API
So I have a rest api that returns some data:
http://localhost:3000/index
returns the following:
[
{
"id": 1,
"magazineTitle": "Title",
"magazineEditie": "Date",
"indexTitle": "Index Title",
"indexSubtitle": "Index Subtitle",
"mediaType": "image", //can also be "video"
"mainMedia": "https://source.unsplash.com/B0iF3I4bLBQ"
}
]
What I want
I want to be able to use this response and populate it over 2 seperate views.
one view will use the data to create a navigation bar
the other view will use it to create a hero header
What I can't seem to understand
Somehow I can't wrap my head around how this would be set up without making this 'illogical'
I feel like loading 2 views with the same model inside my Marionette.Application doesn't make any sense? Or the fact that I fetch my Collections and/or Models there...
I need some help clearing up some Layout issues and best practices I guess.
My code
Besides the fact that I get the data from a localhost url and I have my app setup with webpack this is more or less the code that I am using:
JSFiddle Demo
I have figured out what I needed to do. Based on the documentation (which was kind of confusing me) I figured out a way to render two views with it's needed data.
I was using a CollectionView to read a single data point (1 model) I somehow couldn't figure out a way to immediately get a single Model.
So far the model I had to do:
index.model.js
const IndexModel = Backbone.Model.extend({
urlRoot: "http://localhost:3000/index",
default: {
id: 1,
magazineTitle: "Mag Title",
magazineEditie: "Date",
indexTitle: "Title",
indexSubtitle: "Subtitle",
mediaType: "image",
mainMedia: "http://placehold.it/1900x800/",
},
});
The urlRoot argument here is what I need to do the exact call.
Then I was still confused how to structure my app but I ultimately used Regions and Marionette.View to setup the application.
App.js
export default Marionette.Application.extend({
region: "#content",
onBeforeStart() {
const router = new Router();
},
onStart() {
this.showView(new AppView());
},
});
app.view.js
const AppView = Marionette.View.extend({
tagName: "main",
id: "app",
template: template,
regions: {
navigationRegion: "#main-navigation",
appRegion: "#main-region",
pagesRegion: "#pages-region",
},
initialize() {
this.headerData = new IndexModel({ id: 1 });
this.pagesData = new PagesCollection();
},
onRender() {
this.showChildView("appRegion", new HeroView({ model: this.headerData, }));
this.showChildView("pagesRegion", new PagesView({ collection: this.pagesData, }));
},
});
I had to create a wrapping AppView that utilises regions to specify where child views should render.

Custom data unwrapping in ampersand.js model

I have a model - Configuration:
var Configuration = Model.extend({
props: {
name: 'string'
}
});
In the database, configuration model / table has 3 columns -> id, name and fields. The latter stores site config as a serialized array. When retrieving the entry from the database, I unserialize it and then pass it to the front end, so the front end receives this:
{
"id": 1,
"name": 'global',
"fields": {
"enabled": true,
"site_name": "Test"
}
};
What I want to do is to set whatever is inside fields object as properties on my model, or maybe session so that things get triggered throughout the site when they are updated. To visualize it, I want to achieve something like this:
var Configuration = Model.extend({
props: {
enabled: 'boolean',
site_name: 'string'
}
});
So basically, is there are a way to 'unwrap' stuff in fields object somehow?
The parse method is what you're looking for in this case. See https://github.com/AmpersandJS/ampersand-state/blob/master/ampersand-state.js#L93-L98 It allows you to transform incoming props.

ExtJS 4 - Model containing other model without Id relation

Given is a nested model structure like this:
Model Website
+ id
+ name
+ images[] // List of Image instances
Model Image
+ imageName
+ imageUrl
A serialised version of the response looks like:
{
"id": 4711,
"name": "Some name",
"images" [
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
}
This nested model set is persisted in a document store and is returned on request by Website.id.
There is no by-id-relation to the nested list of images, as they are persisted as a list directly in the parent model. As far as I know, the classic relations in Ext.data.Model refer to the related models via a by-id-relation.
The question is: Is there any way that I can tell the parent model to use the Image model for each of the children in it's images list?
As a first step, you can make your images data to be loaded into the model by using a field type of auto:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{name: 'images', type: 'auto'}
// ... other fields
}
});
Then:
myModel.get('images');
Should return:
[
{"imageName": "Beach", "imageUrl": "http://example.com/whatever.jpg"},
...
]
From there, you should theoretically be able to implement a fully automatized solution to creates the models from this data, and -- the hardest part -- try to keep these created records and the children data in the parent model synchronized. But this is a very involved hack, and a lot of entry points in Ext code base have to be covered. As an illustration, I once tried to do that for "has one" relations, and that represent a lot of code. As a result, I never took the time to consolidate this code, and finally never used it.
I would rather advocate for a simple and local (to the model) solution. You can add a simple method to your model to get the images as records. For example:
Ext.define('My.Model', {
// ...
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Creating a store for the associated model will save you from having to play with the proxy and the reader. It also gives you an interface that is close to Ext's default one for associations.
If you need support for loading images more than once for the same parent record, you can hook on the field's convert method.
Finally, you may also need to handle client-side modifications of associated data, in order to be able to save them to the server. If your associated model allows it, you could simply use the children store's sync method (and don't forget to update the parent model's data in the sync callback!). But if your associated model isn't connected to an endpoint on the server-side, you should be able to hook on the serialize method to save the data in the associated store (as opposed to the one stored in the parent record, that won't get updated if you work with the associated store).
Here's a last example showing both:
Ext.define('My.Model', {
extend: 'Ext.data.Model'
,fields: [
{
name: 'images'
,type: 'auto'
// enables associated data update
,convert: function(data) {
var store = this.imageStore;
if (store) {
store.loadData(data || []);
}
return data;
}
// enables saving data from the associated store
,serialize: function(value, record) {
var store = record.imageStore,
if (store) {
// care, the proxy we want is the associated model's one
var writer = store.proxy && store.proxy.writer;
if (writer) {
return Ext.Array.map(store.getRange(), function(record) {
return writer.getRecordData(record);
});
} else {
// gross implementation, simply use the records data object
return Ext.pluck(store.getRange(), 'data');
}
} else {
return record.get('images');
}
}
}
// ... other fields
}
,getImages: function() {
var store = this.imageStore;
if (!store) {
store = new Ext.data.Store({
model: 'My.ImageModel'
,data: this.get('images') || []
});
this.imageStore = store;
}
return store;
}
});
Please notice that I haven't tested this code, so it might still contains some mistakes... But I hope it will be enough to give you the general idea!

Backbone: Using a form to save model, as well as model relationship into the database

I have been struggling with a form in one of my Backbone views. This form is supposed to save the information for a project model (e.g. project name, project description, project members). While the project-specific information is saved without any issues into the corresponding database table, I did not manage to save the project-user relationships in a joint database table (projects_users, contains the corresponding ids for the two entities). The users that can be added to the project in the form are already present in the database, so nothing needs to be added into the users database table.
Could anyone put me on the right track here? I tried learning about relations in Backbone. These are two of the links that I have already looked into, but could not translate their content into a solution:
Backbone-relational
Model relationships in Rails and Backbone
Thank you,
Alexandra
EDIT
It was suggested that some code from my side would be useful. Since I do not have a good understanding of what I need to do, my code is pretty much a mess right now ... but let me try.
My form view:
App.Views.Projects.Common.Form = Backbone.View.extend({
...
submitted: function(formElement) {
var newData = this.serializeFormData(formElement);
this.model = new App.Models.Project({
name : newData.name,
description : newData.description
// Somehow put the users array associated with the project here ...
});
this.saveFormData(newData);
return false;
},
serializeFormData: function(formElement) {
var fields = formElement.serializeArray();
var serializedData = {};
$.each(fields, function(index, field) {
serializedData[field.name] = field.value;
});
return serializedData;
},
saveFormData: function(newData) {
var project = this.model;
// placeholder for the users that would be associated with the project
// parsing of the data from the form is required to get a corresponding array of user models
var users = App.users;
project.attributes.users = users;
// this line should save the project to the database table and the project-users relationships
// in the projects_users table; it needs the success and error functions
project.save({}, {});
},
...
})
For the project and user model files, I was thinking along these lines:
App.Models.Project = Backbone.RelationalModel.extend({
urlRoot: '/projects',
// Default attributes for the project.
defaults: {
description: "",
users: []
},
relations: [{
type : Backbone.HasMany,
key : 'users',
relatedModel : 'App.Models.User'
}]
});
App.Models.User = Backbone.RelationalModel.extend({
getId: function() {
return this.get('id');
},
getName: function() {
return this.get('name');
},
getEmail: function() {
return this.get('email');
}
});
Although the same information can be found as one of the comments to my question, I was asked to mark this as the answer, to make it easy for other people on StackOverflow. The solution that worked for me can be found here - see my own answer.

Resources