I'm pretty new to ember. I have a basic ember app in place with a CRUD page. I'm having trouble refreshing the view/template of the CRUD page after making a PUT request to a node API using mongoDB.
When I delete a model, the page refreshes fine, but not when I PUT. If I refresh the page, everything is fine and working, but I want the view to refresh as soon as I click the "approve" button I have.
Can someone point me in the right direction of how I should be dealing with this in Ember? Or am I not returning something properly from my API and Ember is doing what it should?
Thanks
Node API PUT:
router.put( '/:id', function( req, res ) {
return Picture.findById( req.params.id, function( err, picture ) {
picture.status = req.body.picture.status;
picture.url = req.body.picture.url;
//...and so on
return picture.save( function( err ) {
if( !err ) { return res.send( picture ); }
return res.send('ERROR');
});
});
});
Model:
App.Picture = DS.Model.extend
authorName: DS.attr('string')
pictureName: DS.attr('string')
url: DS.attr('string')
tags: DS.attr('string')
status: DS.attr('string')
Route:
App.AdminRoute = Ember.Route.extend
model: ->
return #store.find 'picture'
actions:
delete: (picture) ->
picture.destroyRecord() # view updates fine
approve: (picture) ->
picture.set('status', 'verified')
picture.save()
Note - I'm also getting this error in my console that I have no understanding of - I don't remember always getting it though, so I'm not sure how much it's related.
Error: No model was found for 'v'
at new Error (native)
at Error.r (http://localhost:3000/javascripts/libs/ember-1.7.0.js:4:992)
at Ember.Object.extend.modelFor (http://localhost:3000/javascripts/libs/ember-data.js:3:4754)
at t.default.i.extend.extractSingle (http://localhost:3000/javascripts/libs/ember-data.js:1:23642)
at y (http://localhost:3000/javascripts/libs/ember-1.7.0.js:4:30411)
at r [as extractSingle] (http://localhost:3000/javascripts/libs/ember-1.7.0.js:4:28863)
at e.default.Ember.Object.extend.extractSave (http://localhost:3000/javascripts/libs/ember-data.js:1:22390)
at e.default.Ember.Object.extend.extractUpdateRecord (http://localhost:3000/javascripts/libs/ember-data.js:1:22097)
at e.default.Ember.Object.extend.extract (http://localhost:3000/javascripts/libs/ember-data.js:1:21661)
at http://localhost:3000/javascripts/libs/ember-data.js:3:9807
The JSON payload being returned from the server is not in a format suitable for Ember to determine the model type. Ember is expecting something like this:
{
picture: {
"_id":"5428abf33e733af2fc0007ff","authorName":"Ben","pictureName":"Proud Chicken",
"status":"verified","tags":null,"url":"benrlodge.github.io/isotopeSearchFilter/img/four.jpg"
}
}
Since you say it works when you refresh, try comparing this payload with what is returned from the GET. The PUT response should be similar.
Refer to this Ember guide: http://emberjs.com/guides/models/connecting-to-an-http-server/#toc_json-conventions
To tweak the payload and (for example) remove the offending property, you can do this:
App.PictureSerializer = DS.RESTSerializer.extend({
normalizePayload: function(payload) {
if (payload['__v']) {
delete payload['__v'];
}
return this._super(payload);
}
});
This example is for PictureSerializer, but if you made it ApplicationSerializer it would work for any type. See the API here: http://emberjs.com/api/data/classes/DS.RESTSerializer.html#method_normalize
Related
Relative newbie; forgive me if my etiquette and form here aren't great. I'm open to feedback.
I have used create-react-native-app to create an application using PouchDB (which I believe ultimately uses AsyncStorage) to store a list of "items" (basically).
Within a TabNavigator (main app) I have a StackNavigator ("List screen") for the relevant portion of the app. It looks to the DB and queries for the items and then I .map() over each returned record to generate custom ListView-like components dynamically. If there are no records, it alternately displays a prompt telling the user so. In either case, there is an "Add Item" TouchableOpacity that takes them to a screen where they an add a new item (for which they are taken to an "Add" screen).
When navigating back from the "Add" screen I'm using a pattern discussed quite a bit here on SO in which I've passed a "refresh" function as a navigation param. Once the user uses a button on the "Add" screen to "save" the changes, it then does a db.post() and adds them item, runs the "refresh" function on the "List screen" and then navigates back like so:
<TouchableOpacity
style={styles.myButton}
onPress={() => {
if (this.state.itemBrand == '') {
Alert.alert(
'Missing Information',
'Please be sure to select a Brand',
[
{text: 'OK', onPress: () =>
console.log('OK pressed on AddItemScreen')},
],
{ cancelable: false }
)
} else {
this.createItem();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsScreen');
}
}
}
>
And all of this works fine. The "refresh" function (passed as onGoBack param) works fine... for this screen. The database is called with the query, the new entry is found and the components for the item renders up like a charm.
Each of the rendered ListItem-like components on the "List screen" contains a react-native-slideout with an "Edit" option. An onPress for these will send the user to an "Item Details" screen, and the selected item's _id from PouchDB is passed as a prop to the "Item Details" screen where loadItem() runs in componentDidMount and does a db.get(id) in the database module. Additional details are shown from a list of "events" property for that _id (which are objects, in an array) which render out into another bunch of ListItem-like components.
The problem arises when either choose to "Add" an event to the list for the item... or Delete it (using another function via [another] slideout for these items. There is a similar backward navigation, called in the same form as above after either of the two functions is called from the "Add Event" screen, this being the "Add" example:
async createEvent() {
var eventData = {
eventName: this.state.eventName.trim(),
eventSponsor: this.state.eventSponsor.trim(),
eventDate: this.state.eventDate,
eventJudge: this.state.eventJudge.trim(),
eventStandings: this.state.eventStandings.trim(),
eventPointsEarned: parseInt(this.state.eventPointsEarned.trim()),
};
var key = this.key;
var rev = this.rev;
await db.createEvent(key, rev, eventData);
}
which calls my "db_ops" module function:
exports.createEvent = function (id, rev, eventData) {
console.log('You called db.createEvent()');
db.get(id)
.then(function(doc) {
var arrWork = doc.events; //assign array of events to working variable
console.log('arrWork is first assigned: ' + arrWork);
arrWork.push(eventData);
console.log('then, arrWork was pushed and became: ' + arrWork);
var arrEvents = arrWork.sort((a,b)=>{
var dateA = new Date(a.eventDate), dateB = new Date(b.eventDate);
return b.eventDate - a.eventDate;
})
doc.events = arrEvents;
return db.put(doc);
})
.then((response) => {
console.log("db.createEvent() response was:\n" +
JSON.stringify(response));
})
.catch(function(err){
console.log("Error in db.createEvent():\n" + err);
});
}
After which the "Add Event" screen's button fires the above in similar sequence to the first, just before navigating back:
this.createEvent();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsDetails');
The "refresh" function looks like so (also called in componentDidMount):
loadItem() {
console.log('Someone called loadItem() with this.itemID of ' + this.itemID);
var id = this.itemID;
let totalWon = 0;
db.loadItem(id)
.then((item) => {
console.log('[LOAD ITEM] got back data of:\n' + JSON.stringify(item));
this.setState({objItem: item, events: item.events});
if (this.state.events.length != 0) { this.setState({itemLoaded: true});
this.state.events.map(function(event) {
totalWon += parseInt(event.eventPointsEarned);
console.log('totalWon is ' + totalWon + ' with ' +
event.eventPointsEarned + ' having been added.');
});
};
this.setState({totalWon: totalWon});
})
.catch((err) => {
console.log('db.loadItem() error: ' + err);
this.setState({itemLoaded: false});
});
}
I'm at a loss for why the List Screen refreshes when I add an item... but not when I'm doing other async db operations with PouchDB in what I think is similar fashion to modify the object containing the "event" information and then heading back to the Item Details screen.
Am I screwing up with Promise chain someplace? Neglecting behavior of the StackNavigator when navigating deeper?
The only other difference being that I'm manipulating the array in the db function in the non-working case, whereas the others I'm merely creating/posting or deleting/removing the record, etc. before going back to update state on the prior screen.
Edit to add, as per comments, going back to "List screen" and the opening "Item Details" does pull the database data and correctly shows that the update was made.
Further checking I've done also revealed that the console.log in createEvent() to print the response to the db call isn't logging until after some of the other dynamic rendering methods are getting called on the "Item Details" screen. So it seems as though the prior screen is doing the get() that loadItem() calls before the Promise chain in createEvent() is resolving. Whether the larger issue is due to state management is still unclear -- though it would make sense in some respects -- to me as this could be happening regardless of whether I've called my onGoBack() function.
Edit/bump: I’ve tried to put async/await to use in various places in both the db_ops module on the db.get() and the component-side loadItem() which calls it. There’s something in the timing of these that just doesn’t jive and I am just totally stuck here. Aside from trying out redux (which I think is overkill in this particular case), any ideas?
There is nothing to do with PDB or navigation, it's about how you manage outer changes in your depending (already mounted in Navigator since they are in history - it's important to understand - so componentDidMount isn't enough) components. If you don't use global state redux-alike management (as I do) the only way to let know depending component that it should update is passing corresponding props and checking if they were changed.
Like so:
//root.js
refreshEvents = ()=> { //pass it to DeleteView via screenProps
this.setState({time2refreshEvents: +new Date()}) //pass time2refreshEvents to EventList via screenProps
}
//DeleteView.js
//delete button...
onPress={db.deleteThing(thingID).then(()=> this.props.screenProps.refreshEvents())}
//EventList.js
...
constructor(props) {
super(props);
this.state = {
events: [],
noEvents: false,
ready: false,
time2refreshEvents: this.props.screenProps.time2refreshEvents,
}
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.screenProps.time2refreshEvents !== currentState.time2refreshEvents ) {
return {time2refreshEvents : nextProps.screenProps.time2refreshEvents }
} else {
return null
}
}
componentDidMount() {
this._getEvents()
}
componentDidUpdate(prevProps, prevState) {
if (this.state.time2refreshEvents !== prevState.time2refreshEvents) {
this._getEvents()
}
}
_getEvents = ()=> {
//do stuff querying db and updating your list with actual data
}
So I have a bunch of tracks from Spotify's API and I want their genres (which Spotify doesn't give) so for every track I make an API call to Last FM to get their top tags. Now this works for most tracks, I have to match the track name and artist as strings to last fm:
Here's my problem:
I do like this (pseudo:ish code):
let promises = spotifyTracks
.map(track => rp({url: "http://lastfmapi.com/?artist="+track.artist+"?track="+track.name })
.then(response => {
track.genre = response.genre;
return track;
})
);
return Promise.all(promises).then(() => console.log('done!'));
Using request promise.
Now there a few tracks that currrently baffles me. Like 10 in 600. I get a response from lastFM saying:
{ error: 6, message: 'Track not found', links: [] }
To double check I printed the url used:
"http://lastfmapi.com/?artist="+track.artist+"?track="+track.name
Inside the then-call along with the response.
Now if I copied that url from my output and pasted it right into my chrome-browsers address-bar, the API finds the track!?!??!
the actual example
http://ws.audioscrobbler.com/2.0/?method=track.gettoptags&artist=pugh+rogefeldt&track=små+lätta+moln&autocorrect=1&api_key=141bed9ffc180dd9b07ac93b7e3b56d7&format=json
When it is called in my node-code I get
{ error: 6, message: 'Track not found', links: [] }
when called in the chrome address bar I get
{"toptags": {
"tag":
[
{
"count":100,
"name":"swedish",
"url":"https://www.last.fm/tag/swedish"
},
{
"count":100,
"name":"singer-songwriter",
"url":"https://www.last.fm/tag/singer-songwriter"
},
...
],
"#attr":{
"artist":"Pugh Rogefeldt",
"track":"Små lätta moln"
}
}
}
Anyone got any idea what could be the reason behind this discrepancy?
Chrome address bar will encode the string into URL for you, which will make your actual example become
method=track.gettoptags&artist=pugh+rogefeldt&track=sm%C3%A5+l%C3%A4tta+moln&autocorrect=1&api_key=141bed9ffc180dd9b07ac93b7e3b56d7&format=json
You should do the same thing in your node-code with encodeURIComponent
I need to get an image URL from Contentful entry id.
I am getting such an JSON from Contentful query
{
"sys":{
"space":{
"sys":{
"type":"Link",
"linkType":"Space",
"id":"8v1e7eaw70p2"
}
},
"id":"1JfEwVlD9WmYikE8kS8iCA",
"type":"Entry",
"createdAt":"2018-02-28T18:50:08.758Z",
"updatedAt":"2018-02-28T18:50:08.758Z",
"revision":1,
"contentType":{
"sys":{
"type":"Link",
"linkType":"ContentType",
"id":"image"
}
},
"locale":"en-US"
},
"fields":{
"name":"heat",
"image":{
"sys":{
"type":"Link",
"linkType":"Asset",
"id":"6Inruq2U0M2kOYsSAu8Ywk"
}
}
}
}
I am using JS driver they provide:
client.getEntry()
so how to go thru that link: 6Inruq2U0M2kOYsSAu8Ywk ?
Unfortunately, the js SDK will not be able to resolve links when using the single entry endpoint i.e client.getEntry() because there won't be enough data.
When thing I always recommend to work around this is to use the collection endpoint with a query the desired id as a query param. This way you will always get the desired entry with all it's linked data.
Your code should look something like this
client.getEntries({'sys.id': '6Inruq2U0M2kOYsSAu8Ywk'})
.then(response => console.log(response.items[0].fields.image.fields.file.url))
I hope that helps.
Best,
Khaled
Use client.getEntries({'sys.id': '1JfEwVlD9WmYikE8kS8iCA'})
To get the entry fields and the asset fields.
You can also patch the assets to the fields by running this after fetching the data:
/* Patch all the assets to the fields */
const patchAssets = (fields, assets) => {
Object.keys(fields).forEach(function (key) {
let obj = fields[key];
if (obj.sys && obj.sys.linkType === 'Asset') {
const assetId = obj.sys.id;
const matchAsset = assets.find(asset => {
return asset.id === assetId;
});
obj.file = matchAsset;
}
});
return fields;
};
Another way to get image url is to use getAsset('<asset_id>'). So first, using the getEntry() method, you need to get the entry data, then extract the id from the field: fields.image.sys.id, and pass it to the getAsset method.
I want to use pagination from Vuetify framework for VueJS.
Pagination component from Vuetify:
<v-pagination
v-model="pagination.page"
:length="pagination.total / 5"
:total-visible="pagination.visible"
></v-pagination>
I want to execute a function when the user clicks on a button. I want to get the page number and then execute the function with this page number in parameter.
Code from getItems from methods:
this.pagination.page = response.body.page
this.pagination.total = response.body.total
this.pagination.perPage = response.body.perPage
Data:
data () {
return {
items: [],
pagination: {
page: 1,
total: 0,
perPage: 0,
visible: 7
}
}
}
checkout the docs on the events section. I found the input event to handle new page.
<v-pagination
v-model="pagination.page"
:length="pagination.pages"
#input="next"
></v-pagination>
and my next method:
next (page) {
api.getBillList(page)
.then(response => {
this.bills = response.data.content
console.log(this.bills)
})
.catch(error => {
console.log(error)
})
}
COMMENT:
Before you implement pagination, try to see if you really need it in the first place, or you can use alternatives:
https://slack.engineering/evolving-api-pagination-at-slack-1c1f644f8e12
https://dzone.com/articles/why-most-programmers-get-pagination-wrong
http://allyouneedisbackend.com/blog/2017/09/24/the-sql-i-love-part-1-scanning-large-table/
https://www.xarg.org/2011/10/optimized-pagination-using-mysql/
https://www.eversql.com/faster-pagination-in-mysql-why-order-by-with-limit-and-offset-is-slow/
**ANSWER:**
You can react on pagination.page change with watcher since pagination.page changes on button click, and then execute your method.
watch: {
"pagination.page": (newPage) => {
this.onPageChange(newPage);
}
}
Or react on component's input event:
<v-pagination
#input="onPageChange"
></v-pagination>
I arrived here after searching for an error I received trying to implement this pagination in my VueJS project: [Vue warn]: Invalid prop: custom validator check failed for prop "length"..
My problem, and it looks like a problem you may have in your question's example code, was my calculation of length was arriving at a decimal answer. For example, if I had 23 records and a page size of 5, it would return 4.6, giving me the error above. I had to wrap my calculation in a Math.ceil() to arrive at the appropriate value for length.
Hope this helps someone :)
<v-pagination
v-model="currPage"
:length="Math.ceil(arr.length / pageSize)"
:total-visible="6"
></v-pagination>
I am creating a blog with Node, Express and MongoDB. I'm using Mongoose to connect to MongoDB.
I have a create new post form that creates and saves new posts in MongoDB just fine.
When creating a post you can mark the post as published or leave that option unchecked. When you save the post I want you to either:
A) Be redirected to the home page if the post was published, or
B) be redirected to the post's edit/update page if the post was not marked to be published.
Here's the code in the view that I'm trying to use to accomplish the above:
addPost: function(req, res) {
return new Post(req.body.post).save(function() {
if (req.body.published === true) {
return res.redirect("/");
} else {
return res.redirect("/office/post/" + [NEED OBJECT ID HERE] + "/edit");
}
});
}
This is the corresponding view that sends the POST data:
form.web-form(method="post", action="/post/new")
fieldset.fieldset
label.form-label(for="title") Title
input.text-input(id="title", type="text", name="post[title]", placeholder="Post title")
input.text-input(id="alias", type="hidden", name="post[alias]")
label.form-label(for="subhead") Subhead
input.text-input(id="subhead", type="text", name="post[subhead]", placeholder="Post subhead")
label.form-label(for="preview") Preview
textarea.text-area(id="preview", name="post[preview]", rows="4", placeholder="Preview")
label.form-label(for="post-body") Body
textarea.text-area(id="post-body", name="post[body]", rows="5", placeholder="Main content")
input.check-box(onclick="changeButton()", id="published", type="checkbox", name="post[published]")
label.inline-label(for="published") Publish
input.btn-submit(id="submit-post", type="submit", value="Save!")
a.btn-cancel(href="/") Cancel
Any help is greatly appreciated! Thanks!
Like this?
addPost: function(req, res) {
// strip 'post[' and ']' from submitted parameters
var params = {};
for (var k in req.body)
{
params[k.substring(5, k.length - 1)] = req.body[k];
};
var post = new Post(params);
return post.save(function() {
if (params.published === true) {
return res.redirect("/");
} else {
return res.redirect("/office/post/" + post._id + "/edit");
}
});
}