I have a vue router with two components,a list and a detail view, with a link back to the list view. On the same page I have a list of the same data that is shown in the list-view component.
The data is fetched via ajax, and as such is not ready when the router-view is drawn. When the data is ready however, the non-router list is updated, but the router view is not.
How do I communicate to the router view that it should redraw with new data?
If I click on an item in the non-router list, the router-view changes to the details component as expected, and if I then click the "show list" it changes back to the list-component, but this time it is populated with the right data.
I have created a js-fiddle that contains the relevant code: https://jsfiddle.net/pengman/jkwvphf9/2/
(It fakes the ajax by using setTimeout, but the effect is the same)
Html code:
<div id="app">
Router-view:
<router-view class="view"></router-view>
<br>
Unrouted list:
<div class="list-group">
<router-link v-for="plante in planter" class="list-group-item" :to="{ name: 'plante', params: { nummer: plante.Nummer }}">{{ plante.Navn }} | </router-link>
</div>
</div>
<template id="plante-listing-template">
<ul>
<li v-for="plante in planter">
{{ plante.Navn }} <router-link :to="{ name: 'plante', params: { nummer: plante.Nummer }}">Vis</router-link>
</li>
</ul>
</template>
<template id="plante-detail-template">
<div>
plante detail template:
<h3>{{ plante.Navn }}</h3>
<router-link to="/">Show List</router-link>
</div>
<br>
</template>
Javascript code:
var PlanteListing = {
template: '#plante-listing-template',
data: function () {
return {
planter: this.$parent.planter
}
},
watch: {
'$route'(to, from) {
// vi skal opdatere data, saa vi skal beregne igen
this.planter = this.$parent.planter;
},
'dataloaded'() {
this.planter = this.$parent.planter;
}
}
};
var PlanteDetail = {
template: '#plante-detail-template',
data: function () {
var parent = this.$parent;
var nummerFromRoute = this.$route.params.nummer;
//console.log(nummerFromRoute);
var filtered = this.$parent.planter.filter(function (item) {
return (item.Nummer == nummerFromRoute) ? item : false;
});
return {
plante: filtered[0]
}
},
watch: {
'$route'(to, from) {
// vi skal opdatere data, saa vi skal beregne igen
var nummerFromRoute = this.$route.params.nummer;
var filtered = this.$parent.planter.filter(function (item) {
return (item.Nummer == nummerFromRoute) ? item : false;
});
this.plante = filtered[0];
},
}
};
var router = new VueRouter({
mode: 'hash',
base: window.location.href,
routes: [
{ path: '/', component: PlanteListing },
{ name: 'plante', path: '/:nummer', component: PlanteDetail }
]
});
var app = new Vue({
router,
data: {
planter: []
},
components: { PlanteListing: PlanteListing },
methods: {
getJson: function () {
var self = this;
/* Real code:
$.getJSON("content/planter.json", function (param) {
this.planter = param;
}.bind(this));
*/
/* Simulation code: */
setTimeout(function(){self.planter = [ { "Nummer": "0", "Navn": "Bertha Winters" }, { "Nummer": "1", "Navn": "Jeannie Small" }, { "Nummer": "2", "Navn": "Mckay Joyner" }, { "Nummer": "3", "Navn": "Janelle Banks" }, { "Nummer": "4", "Navn": "Bray Moran" }, { "Nummer": "5", "Navn": "Hooper Schwartz" }]; console.log('data loaded')}, 500);
}
},
created: function () {
this.getJson();
}
}).$mount('#app');
Typically you do not want to have a component reach out of itself to get data (as you are doing with this.$parent.planter). Instead, you want to pass it props. To that end, I've modified your code a bit.
The first thing is I upgraded your vue-router to the latest version. This allows you to use the props argument on routes.
var router = new VueRouter({
mode: 'hash',
base: window.location.href,
routes: [
{ path: '/', component: PlanteListing },
{ name: 'plante', path: '/:nummer', component: PlanteDetail, props: true }
]
});
Secondly, you are using planter in all your routes, so I have provided it as a property on the router-view.
<router-view class="view" :planter="planter"></router-view>
This allows us to clean up your component routes and add the data they need as props.
var PlanteListing = {
template: '#plante-listing-template',
props:["planter"]
};
var PlanteDetail = {
template: '#plante-detail-template',
props:["planter", "nummer"],
data: function () {
var filtered = this.planter.filter(item => item.Nummer == this.nummer);
return {
plante: filtered[0]
}
}
};
There is no need to tell the router to redraw; because the data are props, Vue just takes care of that for us.
Here is your updated fiddle.
Related
I want my navbar to apply a different style to the current page.
I've built a component that loops through each link. Within the loop I want to pass the current link's url to a method that will check if the current page url is the same as the url of the link. If it is the new style will be applied.
component.js
module.exports = {
data: function() {
return {
links: [
{name: 'Projects', url: "/admin"},
{name: 'Archives', url: "/archives"},
{name: 'Information', url: "/information/1/edit"},
{name: 'Footer', url: "/footer/edit"},
]
}
},
computed: {
currentPage: {
get: function() {
return window.location.pathname
}
},
activePage: {
get: function() {
this.links.forEach((item) => {
item.currentpage = this.currentPage
item.active = false
if (item.url === item.currentpage) { item.active = true}
})
}
}
}
}
component.html
<ul v-for="link in this.links">
<li>
<a :href="link.url" :class="{active: link.active}">{{link.name}}</a>
</li>
</ul>
1) I can't pass a variable from :class to the component computed property or method.
2) In the current scheme, the component renders the array with only one link with the property {active: true}. Yet still the :class won't respond to this.
I have two demos for same library under repo, with demo.
The main difference is that, one is for browser use and the other is for node use.
However browser one will have error
index.js:1 Uncaught SyntaxError: Identifier 'HeaderComp' has already been declared
What is the main cause?
Update:
Please keep in mind I do not declare a variable twice! I also tried to add console log at the top to ensure the script is executed once!
var HeaderComp = {
name: 'HeaderComp',
template: `
Back
{{ r.title }}
`,
mixins: [VueTopDown.VueTopDownItem],
computed: {
routes () {
return [
{ href: '/page', title: 'Page' },
{ href: '/hello-vue', title: 'HelloVue' }
]
}
}
}
var FooterComp = {
name: 'FooterComp',
template: `{{ vueFooter }}`,
mixins: [VueTopDown.VueTopDownItem],
data () {
return {
vueFooter: 'This is Vue Footer'
}
}
}
var ContentComp = {
name: 'ContentComp',
template: ``,
mixins: [VueTopDown.VueTopDownItem],
computed: {
innerHTML () {
var root = document.createElement('div')
root.innerHTML = this[VTDConstants.OUTER_HTML]
return root.querySelector('*').innerHTML
}
}
}
var HelloVue = {
template: `Hello Vue`,
props: ['clazz'],
inheritAttrs: false
}
var Page = {
template: ``,
props: ['clazz', 'innerHTML'],
inheritAttrs: false
}
var router = new VueRouter([
{ path: '/hello-vue', component: HelloVue },
{ path: '/page', component: Page },
{ path: '*', redirect: '/page' }
])
var inst = new Vue({
router,
mixins: [VueTopDown.VueTopDown],
components: {
HeaderComp: HeaderComp,
FooterComp,
ContentComp
},
data () {
return {
[VueTopDown.VTDConstants]: {
'header': HeaderComp,
'footer': FooterComp,
'.content': ContentComp
}
}
}
})
inst.$mount('#app')
Also keep in mind that similar code works fine in node environment but fails in browser!
Doesn't occur if commenting out inst.$mount('#app')
Expect
The expected behavior of browser should be same as that of node.
What i want is, there a list made from json data. When i click on a item, it creates a new list dynamically.
Now when i click a different item in the first list, i want the second list to change depending on data i receive.
html structure is :
div class="subject-list container-list" id="app-1">
<item-subject
v-for="item in subjectlist"
v-bind:item="item"
v-bind:key="item.id"
>
</item-subject>
</div>
//some other code
<div class="exam-list container-list" id="app-2">
<item-exam
v-for="item in examlist"
v-bind:item="item"
v-bind:key="item.id"
>
</item-exam>
</div>
The main.js file is :
//Dummy json data
var subjects_json = { 'subjectlist': [
{ id: 0, text: 'Computer Graphics' },
{ id: 1, text: 'Automata Theory' },
{ id: 2, text: 'Programming in C' }
]};
var exams_json = { 'examlist': [
{ id: 0, text: 'IAT 1' },
{ id: 1, text: 'IAT 2' },
{ id: 2, text: 'Sem 2' }
]};
/*Contains definition of component item-subject...
Its method() contains call to exam component because it will be
called depending on the subject selected dynamically*/
Vue.component('item-subject', {
props: ['item'],
template: '<li v-on:click="showExams" class="subject-list-item">{{
item.text }}</li>',
methods: {
showExams: function(){
// alert(this.item.text)
console.log("Subject Clicked: "+this.item.text)
var app2 = new Vue({
el: '#app-2',
data: exams_json,
methods: {
showStudents: function(){
console.log("exams rendered")
}
}
})
},
}
});
//Contains definition of component item-exam.
Vue.component('item-exam', {
props: ['item'],
template: '<li v-on:click="showStudents" class="exam-list-item">{{ item.text }}</li>',
methods: {
showStudents: function(){
alert(this.item.text)
console.log("exam component executed")
// console.log("Exam Clicked: "+this.item)
}
}
});
//Call to subject component
var app1 = new Vue({
el: '#app-1',
data: subjects_json,
methods: {
showExams: function(){
console.log("subjects rendered")
}
}
})
So what this code does is, when i click on the first list i.e. subjects list, it dynamically renders new exams list.
Now when i click on second list, alert() method is called successfully.
However if i click any of the subject list(first list), now the alert() is not triggered while clicking second list.
Please tell me whats wrong.
I am working with keystone.js on a Project. And I am doing Templating with Handlebars(hbs)
What I want to do: On my index page I want to display a slider(solved this with unslider.js so I only need to be able to display the images and text from slider model) and the 3 newest events(That works).
Here is my code so far:
This is my Event Model
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* Event Model
* ==========
*/
var Event = new keystone.List('Event', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
});
Event.add({
title: { type: String, required: true },
state: { type: Types.Select, options: 'draft, published, archived', default: 'draft', index: true },
author: { type: Types.Relationship, ref: 'User', index: true },
publishedDate: { type: Types.Date, index: true, dependsOn: { state: 'published' } },
image: { type: Types.CloudinaryImage },
content: {
brief: { type: Types.Html, wysiwyg: true, height: 150 },
extended: { type: Types.Html, wysiwyg: true, height: 400 },
},
eventcategories: { type: Types.Relationship, ref: 'EventCategory', many: true },
});
Event.schema.virtual('content.full').get(function () {
return this.content.extended || this.content.brief;
});
Event.defaultColumns = 'title, state|20%, author|20%, publishedDate|20%';
Event.register();
And this my slider Model
var keystone = require('keystone');
var Types = keystone.Field.Types;
/**
* slider Model
* ==========
*/
var Slider = new keystone.List('Slider', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
});
Slider.add({
title: { type: String, required: true },
image: { type: Types.CloudinaryImage },
});
Slider.register();
Both models work correctly in the backend and it should only be a problem in the view... so here come index view
var keystone = require('keystone');
exports = module.exports = function (req, res) {
var view = new keystone.View(req, res);
var locals = res.locals;
// Init locals
locals.section = 'eventblog';
locals.filters = {
eventcategory: req.params.category,
};
// Set locals
locals.section = 'slider';
locals.data = {
titles: [], //maybe this is a problem?
images: [], //maybe this is a problem?
events: [],
eventcategories: [],
}
// locals.section is used to set the currently selected
// item in the header navigation.
locals.section = 'home';
view.on('init', function (next) {
var q = keystone.list('Event').paginate({
page: req.query.page || 1,
perPage: 3,
maxPages: 1,
filters: {
state: 'published',
},
})
.sort('-publishedDate')
.populate('author categories');
if (locals.data.eventcategory) {
q.where('categories').in([locals.data.eventcategory]);
}
q.exec(function (err, results) {
locals.data.events = results;
next(err);
});
});
// Render the view
view.render('index');
};
And here is my index.hbs
<div class="container">
<div class="my-slider">
<ul>
{{#each slider}}
<!-- doesn't loop even once-->
<li>
<img src="{{cloudinaryUrl image width='300' height='300'}}" >
<p>{{title}}</p>
</li>
{{/each}}
</ul>
</div>
<!-- the code below works correctly -->
<div class="events row">
{{# each data.events.results}}
<div class="col-md-4 col-lg-4">
<h3>{{{title}}}</h3>
<p class=" text-muted">{{{categoryList categories prefix="Posted in "}}}
{{#if author.name.first}}by {{author.name.first}}{{/if}}
</p>
{{#if image}}<img src="{{{cloudinaryUrl image height=160 crop='fit' }}}" class="img center-block">{{/if}}
<p>{{{content.brief}}}</p>
{{#if content.extended}}<p class="read-more">Read more...</p>{{/if}}
</div>
{{/each}}
</div>
</div>
I really hope that my question is clear and someone can help me
The code in your route sets locals.data.events which is why you can use it from handlebars. However, you're not setting locals.slider which is why that {{#each slider}} loop doesn't execute.
In your route, you also need to do something like
keystone.list('Slider').model.find().exec(function (err, results) {
locals.sliders = restuls;
next(err);
}
which populates locals.slider so that in you can do {{#each slider}} in your hbs template. The rest of your code should then work fine.
(Disclaimer, I've not actually tested this, but it should work. If not, try and work out what happened. There are plenty of examples of this kind of code in the keystone demo project)
I have a page resource that uses the page title in the url.
App.Router.map(function () {
this.resource('page', {
path: '/:page_id'
});
});
App.PageRoute = Ember.Route.extend({
serialize: function(model) {
return { page_id: model.title};
}
});
That is working fine in this jsbin. However, I would like to have subpages nested in the url like this:
localhost/#/main_page/sub_page
I tried to make a sub resource (jsbin), but I'm not sure if it is the right approach.
App.Router.map(function () {
this.resource('page', {path: '/:page_id'},
this.resource('subpage', {path: '/:page_id/:subpage_id'}));
});
There are two main problems in my attempt: I have to repeat my page view and it doesn't retain the parent page in the url. I'm getting:
localhost/#/undefined/sub_page
Am I heading in the right direction? Can this be accomplished with just one resource?
Thanks in advance!
Have you considered nesting resources?
App.Router.map(function () {
this.resource('page', {path: '/:page_id'}, function(){
this.resource('subpage', {path: '/:subpage_id'});
});
});
This would enable at least the URL structure you asked for, but i am not really sure about your requirements.
Most of the modifications I've made are in your html and template. Please, don't mash up the Handlebars' link-to helper with classic anchors () and don't change the link-to tagname attribute, or atleast think twice before doing so.
First - move the testData to a globally accessible object, so that you can use it in the menu:
Javascript:
App.CustomRoutes = [{
id: 1,
title: 'single'
}, {
id: 2,
title: 'subpages',
pages: [{
id: 3,
title: 'subpage1'
}, {
id: 4,
title: 'subpage2'
}, {
id: 5,
title: 'subpage3'
}]
}];
Html:
<ul class="navbar-nav nav">
{{#each page in App.CustomRoutes}}
<li class='menu-item'>
{{#link-to 'page' page.title}}
{{page.title}}
{{#if page.pages}}
<b class="caret"></b>
{{/if}}
{{/link-to}}
<span class='subpages'>
<ul>
{{#each subpage in page.pages}}
<li>
{{#link-to 'page.subpage' page.title subpage.title}}
{{subpage.title}}
{{/link-to}}
</li>
{{/each}}
</ul>
</span>
</li>
{{/each}}
</ul>
Then I fixed your router declaration. When you define nested routes, the third parameter to this.resource() is a function, much alike the function you pass to App.Router.map().
App.Router.map(function () {
this.resource('page', {path: '/:page_id'},
function() { this.route('subpage', {path: '/:subpage_id'});});
});
Finally, here are the definitions of your routes. Because Subpage is nested in Page, the route must be called PageSubpageRoute. If it was nested in Foo, it would have been FooSubpageRoute.
NOTE: The this.render() call inside a router, has the following parameters: (, ).
App.PageSubpageRoute = Ember.Route.extend({
model: function(params) {
var parentModel = this.modelFor('page');
var res = $.grep(parentModel.pages, function (e) {
if (e.title == params.subpage_id) {
return e;
}
});
return res[0];
},
serialize: function(model) {
return { subpage_id: model.title};
},
renderTemplate: function() {
this.render('subpage');
}
});
App.PageRoute = Ember.Route.extend({
serialize: function(model) {
return { page_id: model.title};
},
model: function (params) {
return $.grep(App.CustomRoutes, function (e) {
if (e.title == params.page_id) {
return e;
}
})[0];
}
});
Here is the code: jsbin