I have a question about caching in PWA.
I would like to let user choose a list to cache. For example:
User creating few lists, and then he choose one, to save for offline.
When he is offline he can only open 2 views:
smth like "u are offline, do you want to open saved list?" (if it exist)
View with saved list.
At this moment, I am caching all views that user visited, but can't cache views with dynamic data.
I'm using PWA.essentials to do PWA.
services.AddProgressiveWebApp(new PwaOptions
{
RegisterServiceWorker = true,
RegisterWebmanifest = false,
Strategy = ServiceWorkerStrategy.NetworkFirst,
RoutesToPreCache = "/, /Home/Offline, /Home/Saved_list",
OfflineRoute="Offline.html"
});
I have created manifest.json. When im using dev mode in chrome, I can see that at this moment, I'm caching all views execpt views with more complicated path (like /Controller/View/something).
I'm saving list, that user choose in a "Offline.json" file, that is cached too,but when user changes list to save, file "offline.json" is still no updated. I mean my PWA doesn't replace it with new one.
So I have a question about how can I save dynamic list to browser cache, and then set offline route to it.
Service Worker Created by pwa.essentials:
(function () {
'use strict';
// Update 'version' if you need to refresh the cache
var version = 'v1.0::NetworkFirst';
var offlineUrl = "Offline.html";
// Store core files in a cache (including a page to display when offline)
function updateStaticCache() {
return caches.open(version)
.then(function (cache) {
return cache.addAll([
offlineUrl,
'/','/Home','/Generate/Select_mode'
]);
});
}
function addToCache(request, response) {
if (!response.ok)
return;
var copy = response.clone();
caches.open(version)
.then(function (cache) {
cache.put(request, copy);
});
}
self.addEventListener('install', function (event) {
event.waitUntil(updateStaticCache());
});
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys()
.then(function (keys) {
// Remove caches whose name is no longer valid
return Promise.all(keys
.filter(function (key) {
return key.indexOf(version) !== 0;
})
.map(function (key) {
return caches.delete(key);
})
);
})
);
});
self.addEventListener('fetch', function (event) {
var request = event.request;
// Always fetch non-GET requests from the network
if (request.method !== 'GET') {
event.respondWith(
fetch(request)
.catch(function () {
return caches.match(offlineUrl);
})
);
return;
}
event.respondWith(
fetch(request)
.then(function (response) {
// Stash a copy of this page in the cache
addToCache(request, response);
return response;
})
.catch(function () {
return caches.match(request)
.then(function (response) {
return response || caches.match(offlineUrl);
})
.catch(function () {
if (request.headers.get('Accept').indexOf('image') !== -1) {
return new Response('<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title id="offline-title">Offline</title><g fill="none" fill-rule="evenodd"><path fill="#D8D8D8" d="M0 0h400v300H0z"/><text fill="#9B9B9B" font-family="Helvetica Neue,Arial,Helvetica,sans-serif" font-size="72" font-weight="bold"><tspan x="93" y="172">offline</tspan></text></g></svg>', { headers: { 'Content-Type': 'image/svg+xml' } });
}
});
})
);
});
})();
You are saving the user selected pages’ list at server side in offline.json and you are caching that file too. As per working of PWA cached resources are served from cache and not from server until cache is removed/cleared.
You can take 2 approach.
Whenever user adds to the offline.json you need enforce service worker to update the offline file.
You can simply store the user selecting in local storage and from there you add pages to cache list.
Related
countSubcategories() function returns [object Promise] where it should return row counts of mapped subcategories.
This code is in vue.js & Laravel, Any suggestions on this?
<div v-for="(cat,index) in cats.data" :key="cat.id">
{{ countSubcategories(cat.id) }} // Here subcategories row counts should be displayed.
</div>
<script>
export default {
data() {
return {
cats: {},
childcounts: ""
};
},
created() {
this.getCategories();
},
methods: {
countSubcategories(id) {
return axios
.get("/api/user-permission-child-count/" + `${id}`)
.then(response => {
this.childcounts = response.data;
return response.data;
});
},
getCategories(page) {
if (typeof page === "undefined") {
page = 1;
}
let url = helper.getFilterURL(this.filterpartnerForm);
axios
.get("/api/get-user-permission-categories?page=" + page + url)
.then(response => (this.cats = response.data));
}
}
};
</script>
As Aron stated in the previous answer as you are calling direct from the template the information is not ready when the template is rendered.
As far as I understood you need to run getCategories first so then you can fetch the rest of your data, right?
If that's the case I have a suggestion:
Send an array of cat ids to your back-end and there you could send back the list of subcategories you need, this and this one are good resources so read.
And instead of having 2 getCategories and countSubcategories you could "merge" then like this:
fetchCategoriesAndSubcategories(page) {
if (typeof page === "undefined") {
page = 1;
}
let url = helper.getFilterURL(this.filterpartnerForm);
axios
.get("/api/get-user-permission-categories?page=" + page + url)
.then(response => {
this.cats = response.data;
let catIds = this.cats.map(cat => (cat.id));
return this.countSubcategories(catIds) // dont forget to change your REST endpoint to manage receiving an array of ids
})
.then(response => {
this.childcounts = response.data
});
}
Promises allow you to return promises within and chain .then methods
So in your created() you could just call this.fetchCategoriesAndSubcategories passing the data you need. Also you can update your template by adding a v-if so it doesn't throw an error while the promise didn't finish loading. something like this:
<div v-if="childCounts" v-for="(subcategorie, index) in childCounts" :key="subcategorie.id">
{{ subcategorie }} // Here subcategories row counts should be displayed.
</div>
Hello!
Based on the provided information, it could be 2 things. First of all, you may try replacing:
return response.data;
with:
console.log(this.childcounts)
and look in the console if you have the correct information logged. If not, it may be the way you send the information from Laravel.
PS: More information may be needed to solve this. When are you triggering the 'countSubcategories' method?
I would do all the intial login in the component itself, and not call a function in template like that. It can drastically affect the performance of the app, since the function would be called on change detection. But first, you are getting [object Promise], since that is exactly what you return, a Promise.
So as already mentioned, I would do the login in the component and then display a property in template. So I suggest the following:
methods: {
countSubcategories(id) {
return axios.get("..." + id);
},
getCategories(page) {
if (typeof page === "undefined") {
page = 1;
}
// or use async await pattern
axios.get("...").then(response => {
this.cats = response.data;
// gather all nested requests and perform in parallel
const reqs = this.cats.map(y => this.countSubcategories(y.id));
axios.all(reqs).then(y => {
// merge data
this.cats = this.cats.map((item, i) => {
return {...item, count: y[i].data}
})
});
});
}
}
Now you can display {{cat.count}} in template.
Here's a sample SANDBOX with similar setup.
This is happen 'cause you're trying to render a information who doesn't comeback yet...
Try to change this method inside created, make it async and don't call directly your method on HTML. Them you can render your variable this.childcounts.
I have a route in which I use Ajax (not Ember Data) to pull a record, a costcentre, off the server. The record is intended to populate a template for subsequent edits.
Before the fetch, in beforeModel, an empty costcentre is created using createRecord. After the model processing is complete, in afterModel, the returned data is used to populate the costcentre object in the Data Store.
The fetch of the data is successful and in the debugger the update of the locally stored DS object can be seen to have worked but the changes are not seen in the template.
How can I get the template to populate with the data returned from the server ?
In the route I have this :
beforeModel: function(transition) {
this.set('ccToEdit', this.store.createRecord('costcentre'));
},
model(params) {
return getCCByCCIdent( this.urlbase,
this.currentMOP.currentMOP,
ENV.APP.MATClientCode,
params.cceIdentifier_to_edit);
},
afterModel(ccs, transition) {
//I'm testing this with an API end point that returns a
//list but there will only ever be one item in the list
this.ccToEdit.setProperties(ccs[0]);
},
The getCCByCCIdent looks like this :
export const getCCByCCIdent = function(urlbase, currentMOP, clientCode, targetCostCentreIdent) {
return new Promise(function (resolve, reject) {
if (targetCostCentreIdent.length == 0)
{
resolve([])
}
else
{
var theUrl = `${urlbase}/costcentres/${currentMOP}/${clientCode}/${targetCostCentreIdent}`;
$.ajax({
type: 'GET',
url: theUrl,
success: function (response) {
resolve(response);
},
error: function (request, textStatus, error) {
reject(error);
}
});
}
})
}
The simplest way to do this would be to do a then() on the promise being returned from your Ajax call, set appropriate values after that and then return your model:
model(params) {
return getCCByCCIdent(
this.urlbase,
this.currentMOP.currentMOP,
ENV.
params.cceIdentifier_to_edit
).then(ccs => {
let costCentre = this.store.createRecord('costcentre');
costCentre.setProperties(ccs[0]);
return costCentre;
});
},
I have SPA application on Vue.js + Laravel. Authorization logic, completely delegated to Laravel app. However, i need check auth status, when routing has changed. I create small class, which responsible for it.
export default {
user: {
authenticated : false
},
check: function(context) {
context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) =>{
console.log(response)
});
}
Within the component has a method that is called when a change url.
beforeRouteEnter (to, from, next) {
next(vm =>{
Auth.check(vm);
if (!Auth.user.authenticated) {
next({path:'/login'});
}
})
}
Function next() given Vue app instance, then check user object. If user false, next() call again for redirect to login page. All it works, but only when the page is already loaded. If i'll reload /account page, there is a redirect to /login page, because request to Api not completed yet, but code will continue execute. Any idea?
Quite simple to do, you need to make your code work asynchronously and hold routing before request is completed.
export default {
user: {
authenticated : false
},
check: function(context) {
return context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) => {
console.log(response)
});
}
}
then
beforeRouteEnter (to, from, next) {
next(vm => {
Auth.check(vm).then(() => {
if (!Auth.user.authenticated) {
next({path:'/login'});
} else {
next()
}
}
})
}
Other pro tips
Display some loading indicator when loading so your application doesn't seem to freeze (you can use global router hooks for that)
If you are using vue-resource, consider using interceptors (perhaps in addition to the routing checks)
Consider using router.beforeEach so that you don't have to copy-paste beforeRouteEnter to every component
Done. Need to return promise like that
check: function(context) {
return context.$http.get('/api/v1/user').then((response) => {
if (response.body.user != null) {
this.user.authenticated = true
}
}, (response) =>{
console.log(response)
});
}
and then
beforeRouteEnter (to, from, next) {
Auth.check().then(()=>{
if(!Auth.user.authenticated)
next({path:'/login'})
else
next();
})
}
Im trying to get my json result into my react code
The code looks like the following
_getComments() {
const commentList = "AJAX JSON GOES HERE"
return commentList.map((comment) => {
return (
<Comment
author={comment.author}
body={comment.body}
avatarUrl={comment.avatarUrl}
key={comment.id} />);
});
}
How do i fetch AJAX into this?
First, to fetch the data using AJAX, you have a few options:
The Fetch API, which will work out of the box in some browsers (you can use a polyfill to get it working in other browsers as well). See this answer for an example implementation.
A library for data fetching (which generally work in all modern browsers). Facebook recommends the following:
superagent
reqwest
react-ajax
axios
request
Next, you need to use it somewhere in your React component. Where and how you do this will depend on your specific application and component, but generally I think there's two scenarios to consider:
Fetching initial data (e.g. a list of users).
Fetching data in response to some user interaction (e.g. clicking a
button to add more users).
Fetching initial data should be done in the life-cycle method componentDidMount(). From the React Docs:
var UserGist = React.createClass({
getInitialState: function() {
return {
username: '',
lastGistUrl: ''
};
},
componentDidMount: function() {
this.serverRequest = $.get(this.props.source, function (result) {
var lastGist = result[0];
this.setState({
username: lastGist.owner.login,
lastGistUrl: lastGist.html_url
});
}.bind(this));
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function() {
return (
<div>
{this.state.username}'s last gist is
<a href={this.state.lastGistUrl}>here</a>.
</div>
);
}
});
ReactDOM.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
mountNode
);
Here they use jQuery to fetch the data. While that works just fine, it's probably not a good idea to use such a big library (in terms of size) to perform such a small task.
Fetching data in response to e.g. an action can be done like this:
var UserGist = React.createClass({
getInitialState: function() {
return {
users: []
};
},
componentWillUnmount: function() {
this.serverRequest && this.serverRequest.abort();
},
fetchNewUser: function () {
this.serverRequest = $.get(this.props.source, function (result) {
var lastGist = result[0];
var users = this.state.users
users.push(lastGist.owner.login)
this.setState({ users });
}.bind(this));
},
render: function() {
return (
<div>
{this.state.users.map(user => <div>{user}</div>)}
<button onClick={this.fetchNewUser}>Get new user</button>
</div>
);
}
});
ReactDOM.render(
<UserGist source="https://api.github.com/users/octocat/gists" />,
mountNode
);
Lets take a look on the fetch API : https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
Lets say we want to fetch a simple list into our component.
export default MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
lst: []
};
this.fetchData = this.fetchData.bind(this);
}
fetchData() {
fetch('url')
.then((res) => {
return res.json();
})
.then((res) => {
this.setState({ lst: res });
});
}
}
We are fetching the data from the server, and we get the result from the service, we convert is to json, and then we set the result which will be the array in the state.
You can use jQuery.get or jQuery.ajax in componentDidMount:
import React from 'react';
export default React.createClass({
...
componentDidMount() {
$.get('your/url/here').done((loadedData) => {
this.setState({data: loadedData});
});
...
}
First I'd like to use fetchAPI now install of ajax like zepto's ajax,the render of reactjs is asyn,you can init a state in the constructor,then change the state by the data from the result of fetch.
I am still learning Angular JS and have this controller which is making two ajax requests to the lastfm api using different parameters. I want to know when each request has been finished, so that I can display a loading indicator for both requests. I have researched it and read about promises and the $q service but cant get my head around how to incorporate it into this. Is there a better way to set this up? and how can I know when each request is done. Thanks.
angular.module('lastfm')
.controller('ProfileCtrl', function ($scope, ajaxData, usersSharedInformation, $routeParams) {
var username = $routeParams.user;
//Get Recent tracks
ajaxData.get({
method: 'user.getrecenttracks',
api_key: 'key would go here',
limit: 20,
user: username,
format: 'json'
})
.then(function (response) {
//Check reponse for error message
if (response.data.message) {
$scope.error = response.data.message;
} else {
$scope.songs = response.data.recenttracks.track;
}
});
//Get user info
ajaxData.get({
method: 'user.getInfo',
api_key: 'key would go here',
limit: 20,
user: username,
format: 'json'
})
.then(function (response) {
//Check reponse for error message
if (response.data.message) {
$scope.error = response.data.message;
} else {
$scope.user = response.data.user;
}
});
});
I have this factory which handles all the requests
angular.module('lastfm')
.factory('ajaxData', function ($http, $q) {
return {
get: function (params) {
return $http.get('http://ws.audioscrobbler.com/2.0/', {
params : params
});
}
}
});
Quite easy using $q.all(). $http itself returns a promise and $q.all() won't resolve until an array of promises are resolved
var ajax1=ajaxData.get(....).then(....);
var ajax2=ajaxData.get(....).then(....);
$q.all([ajax1,ajax2]).then(function(){
/* all done, hide loader*/
})