Due to data being loaded via AJAX I need my data to be updated when data arrives.
On page load I collect projects in database. Then load data for Tasks and Tags depending on which project is selected (self.SelectedProject).
self.Projects = ko.observableArray();
self.Tasks = ko.observableArray();
self.Tags = ko.observableArray();
self.SelectedProject = ko.observable(); // Chosen Project-object...
For initialization I load data for the first Project:
self.SelectedProject(self.Projects()[0]); // Choose first returned Project...
Then I go on to populate my tag-helping arrays:
ko.computed(function () {
// must be ko.computed as else will not update when data arrives for Tags and Tasks (which are likely to be empty at load time)...
// Empty projectAvailableTags before refill...
self.SelectedProject().projectAvailableTags([]);
// First populate current project's "projectAvailableTags"-array with values...
for (var j = 0, jlen = self.Tags().length; j < jlen; j++) {
self.SelectedProject().projectAvailableTags().push(self.Tags()[j].TagName());
}
for (var i = 0, ilen = self.Tasks().length; i < ilen; i++) {
//---- Populate each TaskTag-array with Tags...
for (var j = 0, jlen = self.Tags().length; j < jlen; j++) {
if (self.Tags()[j].TagTaskId() === self.Tasks()[i].TaskId) {
self.Task()[i].TaskTags.push(self.Tags()[j]);
// Populate the different tag-Arrays...
var tagtype = self.Tags()[j].TagType;
switch (tagtype()) {
case 0: self.Tasks()[i].Location().push(self.Tags()[j].TagName()); break;
case 1: self.Tasks()[i].Manager().push(self.Tags()[j].TagName()); break;
case 2: self.Tasks()[i].Employee().push(self.Tags()[j].TagName()); break;
}
}
}
};
});
This probably look strange and maybe I am doing it unnecessarily complicated.
I use http://aehlke.github.com/tag-it/ as tag manager and it needs an array with TagNames only. Thus I haven´t figured out how to use the Tags()-array directly although I´d like that.
Tasks are presented in an accordion, and I want the Task-tags to be applied in the content panel, while I use my Project-tags as the tagSource for autocomplete-functionality...
But I cannot figure out why my tags are applied 2 times with the ko.computed while they aren´t applied unless I reselect the project without it.
I think you're kind of missing the point of computed observables. The only real distinction between computed observables and a regular function is that you can bind to a computed observable and rely on it to auto-update any time one of its components changes.
The example in the knockout documentation for computed observables uses First/Last name, which is a good example.
Based on this, it's really not a good idea to update the dependency of a computed within that computed itself. In earlier version of knockout this would have actually create an infinite circular reference.
I assume the computed is running twice because Tags and Tasks are both receiving new data, which is triggering an update (but only one update because of the safeguards built into knockout).
A better option would be to subscribe to SelectedProject. Then every time that observable changes you can re-rack all your arrays.
self.SelectedProject.subscribe(function(newValue) {
<load your arrays here>
});
Related
I have an MVC project in where I have two selectlists: one that is displayed to the user, and one that is hidden that contains my viewdata that the displayed one pulls from.
My Javascript function that triggers onchange for another selectlist is as follows:
function select()
{
document.getElementById("SelectedServer").innerHTML = document.getElementById("ServerSelect").value;
var services = document.getElementById("ServiceSelect");
services.options = serviceoptions.options;
console.log(services.options === serviceoptions.options)
var servers = document.getElementById("ServerSelect").selectedOptions;
for (i = 0; i < services.length; i++)
{
for (j = 0; j < servers.length; j++)
{
if (services.options[i].value == servers.item(j).value) { }
else (services.options[i] = null)
}
}
}
This function updates a text field with the selected servers, assigns my services selectlist with the options from the hidden data selectlist called "serviceoptions", and then sorts it so that the services are displayed only if they match the selected server (using value).
For some reason, when I do my options assignment (services.options = serviceoptions.options;), it runs without errors but clearly does not assign the options correctly.
The console output I put to test returns false, and chrome debugger shows that the length has not been updated.
I want to assign the options variable of serviceoptions to the options variable of services. Why can I not do this?
The issue I'm trying to solve with the hidden field is that before, my services would correctly disappear if they did not match, but I had no way to bring them back if the user selected another server.
I cannot use ajax to make another API call to retrieve the servers/services again.
Introduction
I coded a portfolio website for a friend of mine as a university project.
I started to learn Vue.js and started to dive into JavaScript in general.
http://janpzimmermann.com
In some cases I'm still struggling with all the new stuff. Therefore, I'm mixing Vue.js with jQuery and JavaScript. I know this isn't best practice.
But after spending years and years with mostly html and css (and sometimes a little PHP) some things are still new to me. ;)
The Problem
I created a gallery grid (the content is loaded via Vue) and wanted to be able to filter the content via navigation.
Therefore, I came across the following method:
https://www.w3schools.com/howto/howto_js_filter_elements.asp
/* content filter */
filterSelection("all");
function filterSelection(c) {
var x, i;
x = document.getElementsByClassName("content-filter");
if (c == "all") c = "";
// Add the "view" class (display:block) to the filtered elements, and remove the "view" class from the elements that are not selected
for (i = 0; i < x.length; i++) {
w3RemoveClass(x[i], "view");
if (x[i].className.indexOf(c) > -1) w3AddClass(x[i], "view");
}
}
// Show filtered elements
function w3AddClass(element, name) {
var i, arr1, arr2;
arr1 = element.className.split(" ");
arr2 = name.split(" ");
for (i = 0; i < arr2.length; i++) {
if (arr1.indexOf(arr2[i]) == -1) {
element.className += " " + arr2[i];
}
}
}
// Hide elements that are not selected
function w3RemoveClass(element, name) {
var i, arr1, arr2;
arr1 = element.className.split(" ");
arr2 = name.split(" ");
for (i = 0; i < arr2.length; i++) {
while (arr1.indexOf(arr2[i]) > -1) {
arr1.splice(arr1.indexOf(arr2[i]), 1);
}
}
element.className = arr1.join(" ");
}
Unfortunately, there seems to be a bug.
When I open a project, don't close it with the close button and navigate to a new category and open a project there, the project which was opened before is added to the DOM again (even if it doesn't belong to the category!).
I neither couldn't find the bug, yet. Nor I was able to be sure it's not a fault of Vue.
But I tried to replace the JavaScript filter with a jQuery one (this one worked with data-attributes), sadly this wasn't working for me. As I just could add one attribute per item. But sometimes a project belongs to more than one category. (this one: https://jsfiddle.net/k5g6wcw3/21/)
// Variable
var posts = $('.post');
posts.hide();
// Click function
$( ".sort" ).click(function() {
// Get data of category
var customType = $( this ).data('filter'); // category
console.log(customType);
console.log(posts.length); // Length of articles
posts
.hide()
.filter(function () {
return $(this).data('cat') === customType;
})
.show();
});
// All
$( "#showAll" ).click(function() {
$( ".post" ).show();
});
Further thoughts
I know this should be also possible to do with vue routes and maybe vuex, but couldn't find a way how to do it which was understandable to me.
thanks
Mixing Vue and jQuery like this is going to make things much, much, much more difficult than they need to be. The problem you ran into with your first filter was because Vue didn't know about the DOM modifications your javascript filter was doing, so overwrote them on its next update. You're going to have exactly the same problem with the jQuery filter, if you get it working.
So don't do that.
Right now you're letting Vue draw a full list of items, then trying to crawl through the DOM after the fact adding data attributes and hiding the elements you want filtered out. This is a lot of extra work (for both you and the browser), and will fail whenever Vue does a redraw (because it will blow away the changes you made externally.)
Instead, put the attributes in the data you're feeding to Vue, and let Vue filter it before it draws the DOM. Which is a thing that is pretty simple to do in Vue.
(You had mentioned the need for multiple categories per project; here's a quick example of a computed property for that:
data: {
currentFilter: 'photo', // set this from a route param, or a v-model, or whatever
projects: [
{name: "Project one", categories: ['photo', 'book']},
{name: "Project two", categories: ['website']},
{name: "Project 3", categories: ['photo']}
// ...
]
},
computed: {
filteredProjects() {
return this.projects.filter(project => {
return project.categories.indexOf(this.currentFilter) > -1
})
}
}
Have your template v-for over filteredProjects instead of projects, and you're done. Whenever data.currentFilter changes, the list will redraw, filtered by the new value, automatically.)
It's possible to use jQuery from within Vue, but it requires a pretty good understanding of what the framework is doing so you don't wind up creating conflicts between it and your external code. (And I've yet to find a case where it wasn't simpler to just rewrite the jQuery thing as a Vue component anyway.) The same is true of other modern frameworks like React or Angular; these all work on a very different model than the DOM-first strategy jQuery tends to fall back on. Especially when learning, you'll have a much easier time of it if you stick to just one framework at a time instead of trying to mix things together.
I am trying to use a dom repeat template with a custom element wrapping highcharts (https://github.com/avdaredevil/highcharts-chart). It is mostly working except when the data gets changed the charts do not reflect it.
The dom-repeat template:
<template id="scenarioCharts" is="dom-repeat" items="{{chartOptions}}" as="chartOption">
<highcharts-chart highchart-options="{{chartOption}}" />
</template>
The Polymer code to build chartOptions (which is a property with notify true):
observers: [
'buildChartOptions(scenarios)',
],
buildChartOptions(scenarios) {
var i = 0;
this.set('chartOptions', []);
for (i = 0; i < scenarios.length; i += 1) {
this.push('chartOptions', buildCustomChartOptions(scenarios[i]));
}
},
If I remove the line this.set('chartOptions', []); the dom-repeat keeps the old ones and adds the new charts. I have also tried a lot of different things with splices and notifySplices but have had no luck producing the desired result which is the old charts being replaced by the new ones.
Thanks
I would refer to the same bug mentioned by tony19.
Your code seems to be a good workaround, though I'd probably create a local array, populate it and set it again, like this:
buildChartOptions(scenarios) {
var newArray = [];
for (var i = 0; i < scenarios.length; i += 1) {
newArray.push(buildCustomChartOptions(scenarios[i]));
}
this.set('chartOptions', newArray);
}
Another alternative would be to push your changes to the array and then force the dom-repeat to render, by doing a this.$.scenarioCharts.render();. You'd need to try it though, I'm not sure it would work:
buildChartOptions(scenarios) {
for (var i = 0; i < scenarios.length; i += 1) {
this.push('chartOptions',buildCustomChartOptions(scenarios[i]));
}
this.$.scenarioCharts.render();
}
Additionally, even though it does not concern your question, I would recomend declaring a function in polymer like this:
buildChartOptions: function(scenarios) { }
instead of
buildChartOptions(scenarions) { }
I sometimes forget and do the same you did, and the result is compatibility issues with Internet Explorer or Firefox... However if you do as suggested, everything will work fine.
I am working on a reporting related project, where I need to build lot of reports rendered using KO. All data pulled using AJAX and the model is updated. Currently I am writing tons of js functions to map the models. Something like:
function modelx(child) {
var self = this;
self.Name = ko.observable(child.Name);
self.Relation = ko.observable(child.Relation);
// hundred other properties
};
function modely(child) {
var self = this;
self.Age = ko.observable(child.Age);
self.Relation = ko.observable(child.Relation);
// hundred other properties
};
and after AJAX call, I am filling the observable arrays
for (var i = 0; i < jsn.length; i++)
{
VM.modelxlist().push(new modelx(jsn[i]));
}
for (var i = 0; i < jsn1.length; i++)
{
VM.modelylist().push(new modely(jsn1[i]));
}
Is there any way to avoid the definition of modelx, modely,... such that the model is automatically built without loosing the benefits of this approach while using in HTML? Of course there could be a corner case where I may not get a specific property from server, which I should check on the server side.
Also, at times I may need to add additional computed observables (just to be more flexible)
Why don't you use knockout mapping plugin:
http://knockoutjs.com/documentation/plugins-mapping.html
You would then have something like:
var modelxInstance= ko.mapping.fromJS(child);
There are a few mapping plugins for knockout, the one i like the most is actually this one:
https://github.com/LucasLorentz/knockout.mapper
And the reason is that it is more configurable and it is faster.
I think this is what you want..
With ko.mapping.fromJS method u can automatically observe all the properties from your object..
Take some time to read about that..
Absolute beginner here. I want to load data into models as soon as the page loads.
Before anything else is executed. At the moment I have this code.
// Model code
var Portfolio = Spine.Model.sub({});
Portfolio.configure("Portfolio")
Portfolio.extend({
populate: function(values){
for(var i in values[0]){
// add attributes to Model
this.attributes.push(i);
}
for(var j = 0; j < values.length; j++ ){
var tmpInst = this.create(values[j]);
tmpInst.save();
}
}
});
// app controller code
$(function(){
var App =Spine.Controller.sub({
init: function(){
jQuery.getJSON("../xml/data.json",
function(result){
Portfolio.populate(result['content']);
}
).complete(function(result) {
// do other stuff
});
}
})
var app = new App();
});
So when the page has finished loading the controller init function is called, which retrieves the json data and passes it to the Model which parses it and creates the individual instances.
Am I doing this wrong? I have seen Fetch function in the documentation but with no example of how it works.
You might want to use Spine's framework to do this:
Instead of firing your own jQuery.getJSON(), include Spine.Ajax in your Model.
Portfolio.extend(Spine.Model.Ajax);
Set the Spine.Model.host to your server
Add url attribute/method to your Portfolio object to something like 'xml/data.json'
Call Portfolio.fetch()
Override Portfolio.fetch() function to extract just the node of array data or whatever initialization you had in mind just like the configure. If I am not mistaken, fetch() will load the objects and set all the attributes provided in the JSON even if they are not configured in Model #configure call