Angularjs ng-repeat not updating after array update - javascript

I'm not sure what I'm missing here but I'm trying to update a value inside an array, and then have this reflected in the repeater. Do I need to make a copy or something before applying the update in order for Angular to show the changes?
---sample json
"lists": [
{
"name": "one",
"enabled": true
},
{
"name": "two",
"enabled": false
}
]
!-- code
setTimeout(function(){
$scope.$apply(function(){
$scope.lists[1].enabled = true;
});
},1);
!--- html
<span data-ng-repeat="list in lists | filter: {enabled: true}"></span>

From the "other controller" you are talking about broadcast an event:
$rootScope.$broadcast('listChange', listArray);
Then in the controller that controls your $scope.lists value, listen for the event:
$scope.$on('listChange', function(event, list) {
$scope.$apply(function() {
// Update goes here
});
});

It might help if you use angulars built-in timeout function $timeout.
$timeout(function(){
$scope.lists[1].enabled = true;
},1);
Documentation here: https://docs.angularjs.org/api/ng/service/$timeout

Add your lists variable to the $scope inside your controller like this:
$scope.lists = [{
"name": "one",
"enabled": true
},
{
"name": "two",
"enabled": false
}];
You also have to add something inside the <span>, because the Html you posted would not display anything. (You need to do something inside the <span> tag in order to show something)
<span ng-repeat="list in lists | filter:{enabled: true}">{{list.name}}</span>
Now when you update the lists variable, this will be reflected in the Html immediately.
Works fine for me. Hope i could help!

Related

Vue doesn't update DOM upon 'indirectly' changing the value of an expression

TL;DR
I am trying to dynamically build a UI from JSON. The JSON represents a vue.js app with application state (variables) & UI building logic conditional on those variables.
The JSON object of "type": "switch" (see the fiddle linked below), directs the vue.js app to display one of many "cases": {"case1": {..}, "case2": {..}} depending on the value of a state variable "variable": "key" /*translates to vueApp.key */.
Changing one of the variables (update_status) leads to DOM update initially. Changing it again after mounting the app does not affect the DOM, sadly. I'm pretty sure I am doing something stupid or missing something subtle.
Slightly longer version:
(If you're still reading this, please look at the fiddle at this point. None of the below will make sense without it. Thanks!)
Vue.js Template (with app.variables.update_status = "available")
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.variables[data.variable]"
v-if="key == $root.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
Input JSON (bound as data in the above template):
{
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
My question:
Assuming app is the Vue.js app, I'd expect setting app.variables.update_status = "available" should lead to DOM change. But it doesn't as described in TL;DR section. I'm hoping to understand why.
What I have tried:
Made the app watch the entire hierarchy under the object.
I initially thought this is because Vue is unable to monitor object[key] expression where key can change. But its definitely able to do it.
Last value set before mounting the app shows up. After the app is mounted, any change to the the variable sticks but doesn't trigger DOM update. (Annotated with "Doesn't work!" in the fiddle.)
Try it out!
Here's the JS Fiddle (heavily downsized, and commented for easier understanding :))
What to try:
Once the fiddle runs, open the browser console and try executing the following statements:
DEBUG.variables.update_status = "available";
DEBUG.variables.update_status = "checking";
Vue.js version: 2.5.16
Update
Also, I just found out that if I pass data object as:
new Vue({.., data: { .. , variables: {update_status: "temp"}}})
– it works!
I don’t understand this, primarily because variables field is set up to have a deep watcher. I’d assume that when it would have a its fields updated (such as variables.update_status = "new-value";), the observer would eventually trigger the DOM update. But for some reason this doesn’t happen.
I’m really hoping I’m doing something stupid, and that this isn’t this a bug.
Link to the new Fiddle that shows this behaviour: https://jsfiddle.net/g0z3xcyk/
The reason it won't update in your first fiddle is because Vue doesn't detect property addition or deletion, and you're not passing the update_status property when you instance vue, the docs explain it further.
In your second fiddle you're setting update_status when you instance vue and that's why changes, in that case, are detected.
Another option, as mentioned in the docs, is using Vue.set or recreating the object entirely by assigning it again with Object.assign
Some issues with your code:
Check Reactivity In depth as #LuisOrduz commented & answered, Vue cannot detect property addition or deletion. so two solutions:
decalare it first (as your second fiddle did), or uses Vue.set or vm.$set to add one property.
Use vm.$mount(selector) instead of using JQuery to append vm.$el; check vm.$mount
It's better to use vm.$data to access data property instead of vm[key]; check vm.$data
Below is one demo:
function registerComponents() {
Vue.component('all-components', {
template: '#template-all-components',
props: ['data']
});
Vue.component('weave-switch', {
template: '#template-switch',
props: ['data'],
methods: {
toggleStatus: function () {
this.$root.$data.variables.update_status += ' #'
}
}
});
Vue.component('paragraph', {
template: '#template-paragraph',
props: ['data']
});
}
function GenericCard(selector, options) {
var data = Object.assign({}, options.data, {variables: {}});
var watch = {};
Object.keys(data).forEach(function(key) {
watch[key] = {handler: function(val) {
}, deep: true};
});
var app = new Vue({
template: options.template,
data: function () { // uses function instead
return data
},
watch: watch
});
DEBUG = app;
return {
load: function(data) {
Object.keys(data).forEach(function(key) {
app.$data[key] = data[key];
});
//app.$data.variables.update_status = "checking"; // orginal method
app.$set(app.$data.variables, 'update_status', 'checking') // new solution
app.$mount(selector);
//var dom = app.$el;
//$(selector).append(dom); // uses vm.$mount(selector) instead
DEBUG.$set(DEBUG.$data.variables, 'update_status', 'available') // new solution
//DEBUG.$data.variables.update_status = 'available1' // or new solution
//DEBUG.variables.update_status = "available"; // orginal method
},
DEBUG: DEBUG
};
}
registerComponents();
card = GenericCard('#app', {
template: "#template-card",
data: {
ui: {}
}
});
card.load({
ui: {
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
});
Vue.config.productionTip = false
function toggleStatus() {
card.DEBUG.$data.variables.update_status += ' #'
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script type="text/x-template" id="template-all-components">
<div v-if="data.type == 'paragraph'">
<paragraph v-bind:data="data.text"></paragraph>
</div>
<div v-else-if="data.type == 'switch'">
<weave-switch v-bind:data="data"></weave-switch>
</div>
</script>
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<button #click="toggleStatus()">Toggle</button>
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.$data.variables[data.variable]"
v-if="key == $root.$data.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
<script type="text/x-template" id="template-paragraph">
<p>{{data}}</p>
</script>
<script type="text/x-template" id="template-card">
<all-components v-bind:data="ui"></all-components>
</script>
<div id="app">
</div>
<button onclick="toggleStatus()">Toggle2</button>

Populating kendo-tree using from Json

I am in Angular environment using Kendo. All I want to do is following:
Take Json
Produce Kendo tree using it
I have tried it with simple data and it seems to work fine. But this time I have somewhat complex data and it seems like it does not work well with complex Json. I have been trying to have it render Json but it seems like it keeps on thinking and never comes back. I have created a sample Dojo for reference:
http://dojo.telerik.com/EdOqE
I am not sure what am I doing wrong but it just does not seem to work. Can anyone help me with this please?
I presume you have controll over the resultant json, because you'll have to change it a little to fit the TreeView's expected format. Check this out:
{
"items": [{ // Projects
"Id": 0,
"Name": "Your Example Project",
"CreatedOn": "",
"hasChildren": true,
"items": [{ // Analyses
"Id": 0,
"Name": "1.0 - Your Example Run",
"CreatedOn": "",
"hasChildren": true,
"items": [{ // Samples
"Id": 0,
"Name": "Sample 1",
"hasChildren": false,
"Description": "ample frample sample"
}, {
"Id": 0,
"Name": "Sample 2",
"hasChildren": false,
"Description": null
}]
}]
}]
};
The above json is what I did to work in the widget. First of all, the collection properties were renamed to items. All of them, in all levels. With that, kendo will know how property it should deal with. A hasChildren property was added to let it know when it has to show the expand icon. Otherwise it will show the expand option even if the item doesn't haves any children. So user clicks it and get an empty result.
This is the widget initialization options:
{
dataSource: new kendo.data.HierarchicalDataSource({
data: things,
schema: {
data: "items"
}
}),
dataTextField: "Name"
};
With schema.data I tell which property kendo will deal as the collection item. The dataSource expects an array, but if you give him an object, you have to set this property. If it was an array, then kendo would look for item property of each child for default. dataTextField is the name of the property it will use as the label.
Demo
Here is another demo with the data as an array. No need to set schema.data.
Update:
I was afraid you would say that. Yes, there is a way to deal with the data if you can't change it in the server-side. You have to intercept the data at the schema.parse() method and change the resultant data object property to items, so then the widget will understand:
schema: {
data: "items",
parse: function(data) {
if (data.hasOwnProperty("Projects")) {
return { items: data.Projects };
}
else if (data.hasOwnProperty("Analyses")) {
return { items: data.Analyses };
}
else if (data.hasOwnProperty("Samples")) {
return { items: data.Samples };
}
}
}
Demo
Every node when opened will call parse with items collection as data parameter. You have to return a new object with the property name as items instead of Projects, Analysis or Samples.
I forgot you can't touch the data, so can't add hasChildren property as well. Then you have to add a tiny logic into parse to set those properties in each level, otherwise the expand icon would not appear:
schema: {
data: "items",
parse: function(data) {
if (data.hasOwnProperty("Projects")) {
data.Projects.forEach(p => {
p.hasChildren = false;
if (p.hasOwnProperty("Analyses")) {
p.hasChildren = true;
}
});
return { items: data.Projects };
}
else if (data.hasOwnProperty("Analyses")) {
data.Analyses.forEach(a => {
a.hasChildren = false;
if (a.hasOwnProperty("Samples")) {
a.hasChildren = true;
}
});
return { items: data.Analyses };
}
else if (data.hasOwnProperty("Samples")) {
return { items: data.Samples };
}
}
}
Demo
It is ugly, I know. But get used to Kendo, it is the it goes with it.

Dynamically update a select box depending on JSON data with AngularJS

I am quite new to AngularJS and have the following problem: I have two select boxes which I fill with data that is stored in an external JSON file. I want to dynamically fill the second box depending on the item that was selected in the first box. Every item in the first box has a key called relation. I want to use the value of this key as a trigger for dynamically updating the items available in the second box.
Can anyone help me on this? I have already tried to write a function but it doesn't work. Thanx in advance.
This is the JSON structure:
{
"filterElements": [
{
"parameters": [
{
"paraName": "paraOne",
"relation": "default"
},
{
"paraName": "paraTwo",
"relation": "default"
},
{
"paraName": "paraThree",
"relation": "defaultshort"
}
]
},
{
"relations": [
{
"defaultrelation":
[
"equals",
"does not equal",
"greater than",
"less than"
]
},
{
"defaultshort":
[
"equals",
"does not equal"
]
}
]
}
]
}
MyFiddle
Based on you're fiddle, you're missing to bootstrap application. Example:
<body ng-app="list">
...
</body>
After that, you need to register ListCtrl to the existing application.
var app = angular.module('list',[]);
app.controller('ListCtrl', ListCtrl);
function ListCtrl($scope, $http) {
...
}
After doing this, you're controller will be executed, you're request will be made and you can bind the data to your view.
I got it. Here is the solution if anyone is interested:
$scope.change = function (paraSelection) {
$scope.filteredrelations = [];
for(var key in $scope.relations){
if(angular.isDefined($scope.relations[key][paraSelection.relation])){
$scope.filteredrelations = $scope.relations[key] paraSelection.relation];
}
}
};
Complete working example can be found here:
Plunker

Dynamically Load ng-options from API with Angular

I would like to dynamically load select elements from an API request.
Here is my controller:
var myApp = angular.module('myApp',[]).controller('tripCtrl', function($scope){
//Call to API to get people
$scope.people = [
{
"id": 1,
"name": "Joe Hamlet"
},
{
"id": 2,
"name": "Mary Jane"
},
{
"id": 3,
"name": "Tom Lee"
}
];
//Call to API to get the element to load
$scope.selectElement =
{
"Options": "person[dynamicValue] as person[dynamicDisplayName] for person in people",
"DisplayName": "name",
"Value": "id"
};
//Dynamicly load properties
$scope.dynamicValue = $scope.selectElement.DisplayName;
$scope.dynamicDisplayName = $scope.selectElement.Value;
});
HTML:
<select ng-model="selectedPerson" ng-options="{{selectElement.Options}}">
<option value="">Select</option>
</select>
{{selectedPerson}}
I created a JSFiddle trying to accomplish this. http://jsfiddle.net/HB7LU/9493/
I found this question which I was able to implement, but when I tried to set the ng-options from the Element's Options property, it failed to load. When inspected the HTML the code looks to be set properly, but the model binding isn't working.
Edit 12/28/2014:
After updating the Angular version in the original JS Fiddle, it worked properly, however when I expanded to use an actually API, I found another issue with loading ng-options dynamically. Here is the more in depth JS Fiddle: http://jsfiddle.net/zjFp4/330/
Also here is my updated controller. The dataService.getElement() calls a hard coded string, where as the dataService.getElementFromApi() calls the same exact string, just from json-generator (which is the mock API). When inspected the objects set from the API, everything is there, so it must be an issue with the binding in Angular. Any ideas on how to fix this?
function tripCtrl($scope, dataService) {
//Call to API to get people
dataService.getPeople().then(
function (event) {
$scope.people = event;
},
function (s) {
console.log(s); }
);
//Call to API to get the element to load
$scope.selectElement = dataService.getElement();
dataService.getElementFromApi().then(
function (event) {
$scope.apiElement = event;
$scope.dynamicValue = $scope.apiElement.Value;
$scope.dynamicDisplayName = $scope.apiElement.DisplayName;
},
function (s) {
console.log(s); }
);
}

AngularJS: How to create a model which holds an array for a dynamic list of input fields?

I have quite an interesting question (I hope) for all you AngularJS gurus out there. I am looking to create a dynamic list of form input fields based on a SELECT dropdown. As an example, we have a number of categories with each category having a set of specifications which are unique to that category. To help with the explanation we have the following:
Firstly, in the controller we start by initializing the models.
$scope.category = {};
$scope.category.specs = [];
Next we ready the data to be used in the form (actually retrieved from the server via $http). We also initialize a variable to the first element in the categories array.
$scope.categories = [
{ "id": "1", "name": "mobile", specs: [
{ "id": "1", "label": "Operating System" },
{ "id": "2", "label": "Camera type" } ] },
{ "id": "2", "name": "laptop", specs: [
{ "id": "1", "label": "Operating System" },
{ "id": "2", "label": "Graphics Card" } ] }
};
$scope.selectedCategory = $scope.categories[0];
In the form, we have a dropdown which when selected loads the appropriate input fields specific to that category. We use the ngRepeat directive to accomplish this. This is a dynamic list of fields based on $scope.categories.specs. (please note the ???)
<select ng-model="selectedCategory" ng-options="category.name for category in categories"></select>
<div ng-repeat="spec in selectedCategory.specs">
<label>{{spec.label}}</label>
<input type="text" ng-model="???">
</div>
Ultimately, when the user clicks the submit button, we would like to extract the category he/she has selected and then package it together with the specifications they have filled in. The post request should contain something like the following for instance (of course, I only included one spec item, but in reality there would be many):
{ "id": "1", specs [ { "id": "2", "details": "RADEON HD 8970M" } ] }
Unfortunately I am not really sure how to accomplish this. I need to somehow create an array for the spec model, and then ensure that both the ID and user entered data are appropriately extracted... what goes in the ??? and what do we do after? Any help would be much appreciated.
this is how I do it. I make a form, validate it with angular, and then when its valid I submit it with a function.
<form name="signup_form" novalidate ng-submit="signupForm()"></form>
$scope.signupForm = function() {
var data = $scope.signup;
$http({
method : 'POST',
url : 'http://yoursite.com/mail.php',
data : $.param(data), // pass in data as strings
headers : { 'Content-Type': 'application/x-www-form-urlencoded' } // set the headers so angular passing info as form data (not request payload)
})
.success(function(data) {
});
}
also if you want to look at another form validation system for angular check out http://nimbly.github.io/angular-formly/#!/ It may help you solve your current form system.
In the controller, initialize $scope.specDetails as follows:
$scope.specDetails = {};
angular.forEach($scope.categories, function (category, index1) {
$scope.specDetails[category.id] = {};
angular.forEach(category.specs, function (spec, index2) {
$scope.specDetails[category.id][spec.id] = '';
});
});
In the html, replace "???" with specDetails[selectedCategory.id][spec.id]

Categories