Code snippets are written in Pug and Coffeescript
I know about asyncData and it limitations, but what the best practice to get async data from component in Nuxt.js ?
I write some logic in my page, but it's obviously not acceptable since I have cards more than two.
asyncData: ->
axios.defaults.baseURL = 'https://api.github.com/repos/username'
{ data: repo1 } = await axios '/repo1'
{ data: repo4 } = await axios '/repo4'
{ data: repo8 } = await axios '/repo8'
{ data: repo18 } = await axios '/repo18'
{
repo1:
stargazers: repo1.stargazers_count
description: repo1.description
url: repo1.html_url
repo4:
stargazers: repo4.stargazers_count
description: repo4.description
url: repo4.html_url
repo8:
stargazers: repo8.stargazers_count
description: repo8.description
url: repo8.html_url
repo18:
stargazers: repo18.stargazers_count
description: repo18.description
url: repo18.html_url
}
CardSlot(
title='repo1'
:subtitle='repo1.description'
:titleLink='repo1.url'
)
h1 {{repo1.stargazers}}
CardSlot(
title='repo4'
:subtitle='repo4.description'
:titleLink='repo4.url'
)
h1 {{repo4.stargazers}}
CardSlot(
title='repo8'
:subtitle='repo8.description'
:titleLink='repo8.url'
)
h1 {{repo8.stargazers}}
CardSlot(
title='repo18'
:subtitle='repo18.description'
:titleLink='repo18.url'
)
h1 {{repo18.stargazers}}
I want to write only:
CardSlot(
title='repo1'
)
CardSlot(
title='repo4'
)
CardSlot(
title='repo8'
)
CardSlot(
title='repo18'
)
and inside CardSlot component all data magic will happen
add #3: Final push!
so now all I need — is to pass it to the props\slots somehow
in terms of asyncData
asyncData: ({ params }) ->
axios.defaults.baseURL = 'https://api.github.com/repos/username'
repo = 'repo22'
res = await axios('/' + repo)
{
stargazers: res.data.stargazers_count
description: res.data.description
url: res.data.html_url
}
...
h1 {{description}}
I guess I need to define an array, so therefore data can be fetched in a loop, before page loads, and after to pass it somehow to the cards slots
I don't understand your questions !
please could you reformulated it.
but if you want to get many data in one request you could do this =>
export default {
async asyncData ({ params }) {
let users = await axios.get(`https://api/users/`)
let friends = await axios.get(`https://my-api/friends`)
return {
users: users.data
friends: friends.data
}
}
}
according to the docs https://nuxtjs.org/guide/async-data
Thanks !
Disclaimer: No PUG nor Cofeescript :)
Can you make your component dynamic? Inside pages/ create a page called _repo.vue
Now this repo component will have a property in $route.params.repo.
So your asyncData looks like:
asyncData({route}) {
const data = await axios `https://api.github.com/repos/username/${route.params.repo}`
return data
}
That should work.
asyncData just confused me, so I ended up in wrong place, trying to solve unnecessary problems, I realized now — it is completely not suitable for my purposes, so I rework it with plain old vue'ish created function:
<template lang="pug">
.div(
:created="axiSync"
)
h3 asyComp2
h1 {{statusText}}
h1 {{status}}
h3 {{stargazersCount}}
h3 {{description}}
h3 {{htmlUrl}}
h3 {{ratelimit}}
</template>
<script lang="coffee">
import axios from 'axios'
export default
props: ['repo']
data: ->
statusText: ''
status: ''
stargazersCount: ''
description: ''
htmlUrl: ''
ratelimit: ''
url: 'https://api.github.com/repos/userName/'
methods:
axiSync: ->
response = await axios.get(#url + #repo)
#statusText = response.statusText
#status = response.status
#stargazersCount = response.data.stargazers_count
#description = response.data.description
#htmlUrl = response.data.html_url
#ratelimit = response.headers['x-ratelimit-remaining']
</script>
...
asyComp2(
repo = 'niceAndDandy'
)
Thank you to everyone! ^_^
Related
I am building a NextJS based site with multiple locales (different domains) where the data comes from storyblok CMS (folder level translation).
I am trying to figure out the best approach to statically generate the paginated URLs for the blog and since the data is known at build time, I figured the best approach would be to generate all URLs in getStaticPaths and then fetch the data for each Page in getStaticProps. This works fine for routes without parameters but when returning a page parameter along with the slug parameter in getStaticPaths, I cannot access it in getStaticProps.
I know that query params cannot be accessed in getStaticPaths because we cannot know the custom querys at buildtime, but in this specific case, we actually can since these paths are generated in getStaticProps.
pages/[[...slug]].jsx
import {
useStoryblokState,
getStoryblokApi,
StoryblokComponent,
} from "#storyblok/react";
export default function Page({
story,
locale,
locales,
defaultLocale,
stories,
}) {
story = useStoryblokState(story, {
// language: locale,
});
return (
<div>
<StoryblokComponent
blok={story.content}
storyData={story}
stories={stories}
/>
</div>
);
}
export async function getStaticProps({
locale,
locales,
defaultLocale,
params,
}) {
console.log(params.slug); // This logs the slug
console.log(params.page); // This logs undefined
console.log(params.query.page); // This logs undefined
// Empty slug on front page
// Make sure root element page pr folder are selected in storyblok
let slug = params.slug ? params.slug.join("/") : "";
let sbParams = {
version: "draft",
resolve_relations: relationsResolvers,
language: locale,
};
let { data } = await getStoryblokApi().get(
`cdn/stories/${locale}/${slug}`,
sbParams
);
let sbIndexParams = {
version: "draft",
resolve_relations: relationsResolvers,
per_page: 10,
page: params.page || 1,
starts_with: `${locale}/${slug}`,
sort_by: "first_published_at:desc",
language: locale,
filter_query: {
component: {
in: "page,post,case,template",
},
},
};
/* fetch an array of stories if page is startpage */
let storiesData = null;
if (data.story.is_startpage) {
storiesData = await getStoryblokApi().get(`cdn/stories`, sbIndexParams);
}
return {
props: {
story: data ? data.story : false,
key: data ? data.story.id : false,
stories:
data.story.is_startpage && storiesData
? storiesData.data.stories
.filter((story) => story.is_startpage == false)
.map((story) => {
return {
name: story.name,
created_at: story.created_at,
published_at: story.published_at,
id: story.id,
uuid: story.uuid,
slug: story.slug,
full_slug: story.full_slug,
is_startpage: story.is_startpage,
content: {
cover: story.content.cover ?? null,
cover_image: story.content.cover_image ?? null,
author: story.content.author ?? null,
category: story.content.category ?? null,
},
};
})
: false,
locale,
locales,
defaultLocale,
},
revalidate: 3600,
};
}
export async function getStaticPaths({ locales }) {
let { data } = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
filter_query: {
component: {
in: "page,post,case,template",
},
},
});
let paths = [];
Object.keys(data.links).forEach((linkKey) => {
if (data.links[linkKey].is_folder) {
return;
}
// get array for slug because of catch all
const slug = data.links[linkKey].slug;
let splittedSlug = slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// create additional languages
for (const locale of locales) {
if (linkLocale === locale) {
paths.push({ params: { slug: splittedSlug }, locale });
}
}
});
// pagination route generation on custom post types like posts and cases
const per_page = 10;
const startPagesArr = Object.values(data.links)
.map((obj) => obj)
.filter((obj) => obj.is_startpage == true)
.filter((obj) => obj.slug.split("/").length > 2);
// make a loop that loops through all startpages and fetches all stories that are children of that startpage
for (const startPage of startPagesArr) {
let res = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
starts_with: startPage.slug,
paginated: 1,
page: 1,
per_page: per_page,
sort_by: "first_published_at:desc",
filter_query: {
component: {
in: "post,case,template",
},
},
});
let totalPages = Math.ceil(res.total / per_page);
let splittedSlug = startPage.slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// ... Loop through locales and push the paginated pages to the paths Array
for (const locale of locales) {
if (linkLocale === locale) {
for (let i = 2; i <= totalPages; i++) {
paths.push({
params: {
slug: splittedSlug, // this is passed to the getStaticProps function
page: i, //this is not passed to the getStaticProps function
},
locale,
});
}
}
}
}
return {
paths: paths,
fallback: false,
};
}
Accessing the page query param in getStaticProps would solve the problem since I can pass that value to the API request and get the right blogposts to display on the right paginated pages.
Fetching data directly in the component is not preferable for SEO reasons since it will be client-side JS.
All the logic is for the whole site is in the pages/[[...slug.jsx]] file since there are multiple locales, but would it make sense to split it up so I have a dynamic file for the blog itself (across locales)?
I have tried returning the page query param in several different ways, but getStaticProps will only see the param that matches the filename (ex. params.slug will be accessible because the file is called [[...slug]].jsx].
My understanding is that using serializeIds: 'always' will give me this data, but it does not.
Here's what I'm expecting:
{
id="1"
title="some title"
customerId="2"
}
Instead the output I'm receiving is:
{
id="1"
title="some title"
}
My code looks something like this:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// don't want a root prop
root: false,
// true required to have root:false
embed: true,
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always"
});
export function makeServer() {
let server = newServer({
models: {
invoice: Model.extend({
customer: belongsTo()
}),
customer: Model.extend({
invoices: hasMany()
})
},
factories: {
invoice: Factory.extend({
title(i) {
return `Invoice ${i}`;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer")
});
}
}
}),
customer: Factory.extend({
name() {
let fullName = () =>
`${faker.name.firstName()} ${faker.name.lastName()}`;
return fullName;
}
})
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
invoice: ApplicationSerializer.extend({
include: ["customer"]
})
},
routes() {
this.namespace = "api";
this.get("/auth");
}
});
}
Changing the config to root: true, embed: false, provides the correct output in the invoice models, but adds the root and sideloads the customer, which I don't want.
You've run into some strange behavior with how how serializeIds interacts with embed.
First, it's confusing why you need to set embed: true when you're just trying to disable the root. The reason is because embed defaults to false, so if you remove the root and try to include related resources, Mirage doesn't know where to put them. This is a confusing mix of options and Mirage should really have different "modes" that take this into account.
Second, it seems that when embed is true, Mirage basically ignores the serializeIds option, since it thinks your resources will always be embedded. (The idea here is that a foreign key is used to fetch related resources separately, but when they're embedded they always come over together.) This is also confusing and doesn't need to be the case. I've opened a tracking issue in Mirage to help address these points.
As for you today, the best way to solve this is to leave root to true and embed false, which are both the defaults, so that serializeIds works properly, and then just write your own serialize() function to remove the key for you:
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
You should be able to test this out on both /invoices and /invoices/1.
Check out this REPL example and try making a request to each URL.
Here's the config from the example:
import {
Server,
Serializer,
Model,
belongsTo,
hasMany,
Factory,
} from "miragejs";
import faker from "faker";
const ApplicationSerializer = Serializer.extend({
// will always serialize the ids of all relationships for the model or collection in the response
serializeIds: "always",
serialize(resource, request) {
let json = Serializer.prototype.serialize.apply(this, arguments);
let root = resource.models ? this.keyForCollection(resource.modelName) : this.keyForModel(resource.modelName)
return json[root];
}
});
export default new Server({
models: {
invoice: Model.extend({
customer: belongsTo(),
}),
customer: Model.extend({
invoices: hasMany(),
}),
},
factories: {
invoice: Factory.extend({
title(i) {
return "Invoice " + i;
},
afterCreate(invoice, server) {
if (!invoice.customer) {
invoice.update({
customer: server.create("customer"),
});
}
},
}),
customer: Factory.extend({
name() {
return faker.name.firstName() + " " + faker.name.lastName();
},
}),
},
seeds(server) {
server.createList("invoice", 10);
},
serializers: {
application: ApplicationSerializer,
},
routes() {
this.resource("invoice");
},
});
Hopefully that clears things up + sorry for the confusing APIs!
I am working with Angular2 and es5. I want to use http in a service.
Unfortunately I have 2 errors:
- http is undefined, but ng.http.Http is defined,
- I have this error for the main component:
vendor-client.min.js:28 EXCEPTION: Can't resolve all parameters for class0: (t, ?)
Here is my service code:
;(function(app, ng) {
console.log(new ng.http.Http());
app.ApplicationsService = ng.core.Injectable().Class({
constructor: [ng.http.Http, function(http) {
console.log(http);
this.applicationsEmailUrl = 'api/applications/email';
this.http = http;
}],
emailExists: function(email) {
console.log(email);
var data = { email: email };
return this.http.post(this.applicationsEmailUrl, data)
.toPromise()
.then(function(response) { response.json().data; })
.catch(this.handleError);
}
});
})(window.app || (window.app = {}), window.ng);
Here is the main component:
;(function(app, ng) {
app.AppComponent = ng.core
.Component({
selector: 'register-form',
templateUrl: 'src/register/app.component.html'
})
.Class({
constructor: [ng.core.ElementRef, app.ApplicationsService, function(ref, Applications) {
console.log('app.component.js');
this.programs = JSON.parse(ref.nativeElement.getAttribute('programs'));
this.applications = Applications;
}],
emailExists: function(email) {
console.log('emailExists() triggered');
Applications.emailExists(email);
}
});
})(window.app || (window.app = {}), window.ng);
The bootstrap:
;(function(app, ng) {
document.addEventListener('DOMContentLoaded', function() {
ng.platformBrowserDynamic.bootstrap(app.AppComponent, [
ng.forms.disableDeprecatedForms(),
ng.forms.provideForms(),
ng.http.HTTP_PROVIDERS,
app.ApplicationsService
]);
});
})(window.app || (window.app = {}), window.ng);
If I try to inject http into the main component within the providers array, it works. But I would rather prefer to have a service.
I found out the problem. Looks like Angular2 needs to load your code in order. The main component was loaded before the service, so it was undefined. I put all my code in one file and it works. I will use a require loader asap.
Yo! I'm using Redux and Normalizr. The API I'm working with sends down objects that look like this:
{
name: 'Foo',
type: 'ABCD-EFGH-IJKL-MNOP'
}
or like this
{
name: 'Foo2',
children: [
'ABCD-EFGH-IJKL-MNOP',
'QRST-UVWX-YZAB-CDEF'
]
}
I want to be able to asynchronously fetch those related entities (type and children) when the above objects are accessed from the state (in mapStateToProps). Unfortunately, this does not seem to mesh with the Redux way as mapStateToProps is not the right place to call actions. Is there an obvious solution to this case that I'm overlooking (other than pre-fetching all of my data)?
Not sure that I have correctly understood your use-case, but if you want to fetch data, one simple common way is to trigger it from a React component:
var Component = React.createClass({
componentDidMount: function() {
if (!this.props.myObject) {
dispatch(actions.loadObject(this.props.myObjectId));
}
},
render: function() {
const heading = this.props.myObject ?
'My object name is ' + this.props.myObject.name
: 'No object loaded';
return (
<div>
{heading}
</div>
);
},
});
Given the "myObjectId" prop, the component triggers the "myObject" fetching after mounting.
Another common way would be to fetch the data, if it's not already here, from a Redux async action creator (see Redux's doc for more details about this pattern):
// sync action creator:
const FETCH_OBJECT_SUCCESS = 'FETCH_OBJECT_SUCCESS';
function fetchObjectSuccess(objectId, myObject) {
return {
type: FETCH_OBJECT_SUCCESS,
objectId,
myObject,
};
}
// async action creator:
function fetchObject(objectId) {
return (dispatch, getState) => {
const currentAppState = getState();
if (!currentAppState.allObjects[objectId]) {
// fetches the object if not already present in app state:
return fetch('some_url_.../' + objectId)
.then(myObject => (
dispatch(fetchObjectSuccess(objectId, myObject))
));
} else {
return Promise.resolve(); // nothing to wait for
}
};
}
I have attempted implementing search in Telescope using pure javascript, since it looks like FTS is a while off for Meteor to implement and I couldn't get 2.4 playing nicely with Meteor yet.
I'm using the existing pagination model that is already implemented in Telescope to display the Top/New/Best posts, plus a Session variable for the search keyword that is set in the Router when you navigate to e.g. /search/foobar.
However, it doesn't quite seem to be working; when I have, say, 100 posts, the regular paginated subscription only comes back with 25 of these and my search results only show the posts in the first 25.
I've been banging my head against a wall for days trying to debug this one: sometimes it works, sometimes it doesn't!
Here's the code (I've included all additional search code for reference):
app.js:
var resultsPostsSubscription = function() {
var handle = paginatedSearchSubscription( 10, 'searchResults' );
handle.fetch = function() {
return limitDocuments( searchPosts( Session.get( 'keyword' ) ), handle.loaded() );
};
return handle;
};
var resultsPostsHandle = resultsPostsSubscription();
paginated_sub.js:
I duplicated the existing paginatedSubscription because I can't pass a Session var in as an arg; it needs to be dynamic. I'll probably refactor later.
paginatedSearchSubscription = function (perPage/*, name, arguments */) {
var handle = new PaginatedSubscriptionHandle(perPage);
var args = Array.prototype.slice.call(arguments, 1);
Meteor.autosubscribe(function() {
var subHandle = Meteor.subscribe.apply(this, args.concat([
Session.get( 'keyword' ), handle.limit(), function() { handle.done(); }
]));
handle.stop = subHandle.stop;
});
return handle;
}
search.js: (new file, in /common directory)
// get all posts where headline, categories, tags or body are LIKE %keyword%
searchPosts = function( keyword ) {
var query = new RegExp( keyword, 'i' );
var results = Posts.find( { $or: [ { 'headline': query }, { 'categories': query }, { 'tags': query }, { 'body': query } ] } );
return results;
};
publish.js:
Meteor.publish( 'searchResults', searchPosts );
posts_list.html:
<template name="posts_results">
{{> posts_list resultsPostsHandle}}
</template>
posts_list.js:
Template.posts_results.resultsPostsHandle = function() {
return resultsPostsHandle;
};
router.js:
there's a search bar in the nav that redirects to here
posts_results = function( keyword ) {
Session.set( 'keyword' , keyword );
return 'posts_results';
};
Meteor.Router.add({
...
'/search/:keyword':posts_results,
...
})
Any help would be greatly appreciated!
A little late but here is a full write up on how to implement full text search in meteor.
"The simplest way without editing any Meteor code is to use your own mongodb."