Re-render ng-options after second-level change in collection - javascript

Problem
I have a combo box, basically a select element that is filled with an array of complex objects by ng-options. When I update any object of the collection on second-level, this change is not applied to the combo box.
This is also documented on the AngularJS web site:
Note that $watchCollection does a shallow comparison of the properties of the object (or the items in the collection if the model is an array). This means that changing a property deeper than the first level inside the object/collection will not trigger a re-rendering.
Angular view
<div ng-app="testApp">
<div ng-controller="Ctrl">
<select ng-model="selectedOption"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id">
</select>
<button ng-click="changeFirstLevel()">Change first level</button>
<button ng-click="changeSecondLevel()">Change second level</button>
<p>Collection: {{ myCollection }}</p>
<p>Selected: {{ selectedOption }}</p>
</div>
</div>
Angular controller
var testApp = angular.module('testApp', []);
testApp.controller('Ctrl', ['$scope', function ($scope) {
$scope.myCollection = [
{
id: '1',
name: 'name1',
nested: {
value: 'nested1'
}
}
];
$scope.changeFirstLevel = function() {
var newElem = {
id: '1',
name: 'newName1',
nested: {
value: 'newNested1'
}
};
$scope.myCollection[0] = newElem;
};
$scope.changeSecondLevel = function() {
var newElem = {
id: '1',
name: 'name1',
nested: {
value: 'newNested1'
}
};
$scope.myCollection[0] = newElem;
};
}]);
You can also run it live in this JSFiddle.
Question
I do understand that AngularJS does not watch complex objects within ng-options for performance reasons. But is there any workaround for this, i.e. can I manually trigger re-rendering? Some posts mention $timeout or $scope.apply as a solution, but I could utilize neither.

A quick hack I've used before is to put your select inside an ng-if, set the ng-if to false, and then set it back to true after a $timeout of 0. This will cause angular to rerender the control.
Alternatively, you might try rendering the options yourself using an ng-repeat. Not sure if that would work.

Yes, it's a bit ugly and needs an ugly work-around.
The $timeout solution works by giving AngularJS a change to recognise that the shallow properties have changed in the current digest cycle if you set that collection to [].
At the next opportunity, via the $timeout, you set it back to what it was and AngularJS recognises that the shallow properties have changed to something new and updates its ngOptions accordingly.
The other thing I added in the demo is to store the currently selected ID before updating the collection. It can then be used to re-select that option when the $timeout code restores the (updated) collection.
Demo: http://jsfiddle.net/4639yxpf/
var testApp = angular.module('testApp', []);
testApp.controller('Ctrl', ['$scope', '$timeout', function($scope, $timeout) {
$scope.myCollection = [{
id: '1',
name: 'name1',
nested: {
value: 'nested1'
}
}];
$scope.changeFirstLevel = function() {
var newElem = {
id: '1',
name: 'newName1',
nested: {
value: 'newNested1'
}
};
$scope.myCollection[0] = newElem;
};
$scope.changeSecondLevel = function() {
// Stores value for currently selected index.
var currentlySelected = -1;
// get the currently selected index - provided something is selected.
if ($scope.selectedOption) {
$scope.myCollection.some(function(obj, i) {
return obj.id === $scope.selectedOption.id ? currentlySelected = i : false;
});
}
var newElem = {
id: '1',
name: 'name1',
nested: {
value: 'newNested1'
}
};
$scope.myCollection[0] = newElem;
var temp = $scope.myCollection; // store reference to updated collection
$scope.myCollection = []; // change the collection in this digest cycle so ngOptions can detect the change
$timeout(function() {
$scope.myCollection = temp;
// re-select the old selection if it was present
if (currentlySelected !== -1) $scope.selectedOption = $scope.myCollection[currentlySelected];
}, 0);
};
}]);

Explanation of why changeFirstLevel works
You are using (selectedOption.id + ' - ' + selectedOption.name) expression to render select options labels. This means an {{selectedOption.id + ' - ' + selectedOption.name}} expression is working for select elements label. When you call changeFirstLevel func the name of selectedOption is changing from name1 to newName1. Because of that html is rerendering indirectly.
Solution 1
If the performance is not a problem for you you can simply delete the track by expression and the problem will be solved. But if you want performance and rerender at the same time both will be a bit low.
Solution 2
This directive is deep watching the changes and apply it to model.
var testApp = angular.module('testApp', []);
testApp.directive('collectionTracker', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
var oldCollection = [], newCollection = [], ngOptionCollection;
scope.$watch(
function(){ return ngModel.$modelValue },
function(newValue, oldValue){
if( newValue != oldValue )
{
for( var i = 0; i < ngOptionCollection.length; i++ )
{
//console.log(i,newValue,ngOptionCollection[i]);
if( angular.equals(ngOptionCollection[i] , newValue ) )
{
newCollection = scope[attrs.collectionTracker];
setCollectionModel(i);
ngModel.$setUntouched();
break;
}
}
}
}, true);
scope.$watch(attrs.collectionTracker, function( newValue, oldValue )
{
if( newValue != oldValue )
{
newCollection = newValue;
oldCollection = oldValue;
setCollectionModel();
}
}, true)
scope.$watch(attrs.collectionTracker, function( newValue, oldValue ){
if( newValue != oldValue || ngOptionCollection == undefined )
{
//console.log(newValue);
ngOptionCollection = angular.copy(newValue);
}
});
function setCollectionModel( index )
{
var oldIndex = -1;
if( index == undefined )
{
for( var i = 0; i < oldCollection.length; i++ )
{
if( angular.equals(oldCollection[i] , ngModel.$modelValue) )
{
oldIndex = i;
break;
}
}
}
else
oldIndex = index;
//console.log(oldIndex);
ngModel.$setViewValue(newCollection[oldIndex]);
}
}}
});
testApp.controller('Ctrl', ['$scope', function ($scope) {
$scope.myCollection = [
{
id: '1',
name: 'name1',
nested: {
value: 'nested1'
}
},
{
id: '2',
name: 'name2',
nested: {
value: 'nested2'
}
},
{
id: '3',
name: 'name3',
nested: {
value: 'nested3'
}
}
];
$scope.changeFirstLevel = function() {
var newElem = {
id: '1',
name: 'name1',
nested: {
value: 'newNested1'
}
};
$scope.myCollection[0] = newElem;
};
$scope.changeSecondLevel = function() {
var newElem = {
id: '1',
name: 'name1',
nested: {
value: 'newNested2'
}
};
$scope.myCollection[0] = newElem;
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.min.js"></script>
<div ng-app="testApp">
<div ng-controller="Ctrl">
<p>Select item 1, then change first level. -> Change is applied.</p>
<p>Reload page.</p>
<p>Select item 1, then change second level. -> Change is not applied.</p>
<select ng-model="selectedOption"
collection-tracker="myCollection"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id">
</select>
<button ng-click="changeFirstLevel()">Change first level</button>
<button ng-click="changeSecondLevel()">Change second level</button>
<p>Collection: {{ myCollection }}</p>
<p>Selected: {{ selectedOption }}</p>
</div>
</div>

Why don't you just simply track collection by that nested property ?
<select ng-model="selectedOption"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.nested.value">
Update
Since you don't know which property to track you can simply track all properties passing a function on track by expression.
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by $scope.optionsTracker(selectedOption)"
And on Controller:
$scope.optionsTracker = (item) => {
if (!item) return;
const firstLevelProperties = Object.keys(item).filter(p => !(typeof item[p] === 'object'));
const secondLevelProperties = Object.keys(item).filter(p => (typeof item[p] === 'object'));
let propertiesToTrack = '';
//Similarilly you can cache any level property...
propertiesToTrack = firstLevelProperties.reduce((prev, curr) => {
return prev + item[curr];
}, '');
propertiesToTrack += secondLevelProperties.reduce((prev, curr) => {
const childrenProperties = Object.keys(item[curr]);
return prev + childrenProperties.reduce((p, c) => p + item[curr][c], '');
}, '')
return propertiesToTrack;
}

I think that any solution here will be either overkill (new directive) or a bit of a hack ($timeout).
The framework does not automatically do it for a reason, which we already know is performance. Telling angular to refresh would be generally frowned upon, imo.
So, for me, I think the least intrusive change would be to add a ng-change method and set it manually instead of relying on the ng-model change. You'll still need the ng-model there but it would be a dummy object from now on. Your collection would be assigned on the return (.then ) of the response , and let alone after that.
So, on controller:
$scope.change = function(obj) {
$scope.selectedOption = obj;
}
And each button click method assign to the object directly:
$scope.selectedOption = newElem;
instead of
$scope.myCollection[0] = newElem;
On view:
<select ng-model="obj"
ng-options="(selectedOption.id + ' - ' + selectedOption.name) for selectedOption in myCollection track by selectedOption.id"
ng-change="change(obj)">
</select>
Hope it helps.

Related

How to properly share data between angular services

I'm attempting to rewrite a large and complex form that is doing everything in a controller. I started by separating related functions into their own modules/services. I don't understand how I am supposed to maintain the form data without crowding up the controller or requiring an excessive amount of arguments to be passed to the service function.
My current approach is to set variables on the service, then use that service in other services and try to access the saved data. This doesn't seem to be working. I think this is because injecting the service into another creates a new instance without all the saved values.
Here is a plunker that summarizes this approach: https://plnkr.co/edit/vyKtlXk8Swwf7xmoCJ4q
let app = angular.module('myApp', []);
app.service('productService', [function() {
let products = [
{ name: 'foo', value: 'foo' },
{ name: 'bar', value: 'bar' },
{ name: 'baz', value: 'baz' }
];
let selectedProduct = null;
this.getAvailableProducts = function() {
return products;
}
this.setSelectedProduct = function(product) {
selectedProduct = product;
}
}]);
app.service('storeService', ['productService', function(productService) {
let states = [
{ name: 'SC', value: 'SC' },
{ name: 'GA', value: 'GA' },
{ name: 'LA', value: 'LA' }
];
let selectedState = '';
this.getAvailableStates = function() {
return states;
}
this.setSelectedState = function(state) {
selectedState = state;
}
this.getPrice = function() {
// This console.log will always return undefined.
// productService.selectedProduct is not available.
console.log(productService.selectedProduct);
if (productService.selectedProduct == "foo" && selectedState == 'SC') {
return 10;
}
return 5;
}
}]);
app.controller('myController', function($scope, storeService, productService) {
$scope.name = '';
$scope.deliveryState = '';
$scope.selectedProduct = null;
$scope.price = 0;
$scope.productSelection = productService.getAvailableProducts();
$scope.states = storeService.getAvailableStates();
$scope.productChanged = function() {
productService.setSelectedProduct($scope.selectedProduct);
$scope.price = storeService.getPrice();
}
$scope.stateChanged = function() {
storeService.setSelectedState($scope.deliveryState);
$scope.price = storeService.getPrice();
}
});
I am trying to avoid something like this:
$scope.price = storeService.getPrice(
$scope.state,
$scope.selectedProduct,
$scope.servicePackage,
$scope.serviceFee,
$scope.shippingSelection,
// etc…
);
Should I be creating a third service that sets and gets all the data on the other services?
Should I just maintain all the data on the controller?
why do I get undefined when accessing a variable on the injected service?
The let declaration creates a private variable.
Add a getter for the variable:
app.service('productService', [function() {
let products = [
{ name: 'foo', value: 'foo' },
{ name: 'bar', value: 'bar' },
{ name: 'baz', value: 'baz' }
];
let selectedProduct = null;
this.getAvailableProducts = function() {
return products;
}
this.setSelectedProduct = function(product) {
selectedProduct = product;
}
//ADD getter
this.getSelectedProduct = function() {
return selectedProduct;
}
}]);
And use the getter:
this.getPrice = function() {
// This console.log will always return undefined.
// productService.selectedProduct is not available.
console.log(productService.selectedProduct);
̶i̶f̶ ̶(̶p̶r̶o̶d̶u̶c̶t̶S̶e̶r̶v̶i̶c̶e̶.̶s̶e̶l̶e̶c̶t̶e̶d̶P̶r̶o̶d̶u̶c̶t̶ ̶=̶=̶ ̶"̶f̶o̶o̶"̶ ̶&̶&̶ ̶s̶e̶l̶e̶c̶t̶e̶d̶S̶t̶a̶t̶e̶ ̶=̶=̶ ̶'̶S̶C̶'̶)̶ ̶{̶
if (productService.getSelectedProduct() == "foo" && selectedState == 'SC') {
return 10;
}
return 5;
}
Update
Should my services be communicating like that or is there a different, more accepted method?
I am trying to avoid something like this:
$scope.price = storeService.getPrice(
$scope.state,
$scope.selectedProduct,
$scope.servicePackage,
$scope.serviceFee,
$scope.shippingSelection,
// etc…
);
One way to avoid this is use an object as an argument to provide multiple options:
$scope.options = {};
$scope.price = storeService.getPrice(
$scope.selectedProduct,
$scope.options
);
The form can populate the options object directly:
<select ng-model="options.state">
<option ng-repeat="state in states">{{ state.name }}</option>
</select><br>
<select ng-model="options.serviceFee">
<option ng-repeat="fee in feeList">{{ fee.name }}</option>
</select><br>
<!-- //etc... -->
The setting of a variable in one service before computing something in another service creates an undesirable coupling that makes the code difficult to understand, debug, maintain, and test.
Instead all the information needed from the controller should be provided to the pricing service in a coherent manner.
You should not be injecting $scope, $scope is an outdated way of developing AngularJs and you should look into components or controllerAs syntax.
The controller should only be marshalling data between services and your view.
Services should provide data functions like get a product or create a new product and the controller should be doing things like
$ctrl = this;
$ctrl.product = productService.new();
or
$ctrl.product = productService.get(productId);
Then in your view you bind to properties of the product
<input name="name" ng-model="$ctrl.product.name">
And when you save a product you pass the whole thing back to the service
<form name="productForm" ng-submit="productForm.$valid && $ctrl.save()">
and in the controller
$ctrl.save = function() {
productService.save($ctrl.product);
}

Lodash/JS - Convert _.find statement to _.forEach

I'm having trouble converting the following Lodash statement to something that works in an application that I inherited at work and am trying to fix bugs in. At the moment, we are having issues with only one device on our system returning when two devices have the same name and the following code seems to be the culprit as it would only return the first "true" value in an array:
var group = _.find(groupList, {id: id});
How would I successfully convert this to a statement that iterates over all objects in an array and then returns those objects? I've tried the following options to no avail:
var group = _.filter(groupList, {id: id});
and
var group = _.every(groupList, {id: id});
and
var group = _.forEach(groupList, {id: id})
return {id};
I know I am probably missing something in my syntax. Any help would be much appreciated. Running Lodash v3.7.0
Here's the rest of the code in the directive in case anyone is interested or sees something else I might be missing:
define(['./../_module'], function (directives) {
'use strict';
directives.directive('dmGroupedList', ['$compile', '$state', 'APP_CONSTANTS', function ($compile, $state, APP_CONSTANTS) {
return {
restrict: 'A',
scope: {
items: '=',
groupBy: '=',
actions: '=',
nonameGroupLabel: '='
},
templateUrl: function (elem, attrs) {
return attrs.templateUrl || 'views/shared-templates/grouped-list.html';
},
link: function (scope, element, attrs) {
scope.$watchGroup(['items', 'groupBy', 'nonameGroupLabel'], function () {
scope.groupList = [];
scope.groupedItems = {};
var actions = scope.actions[scope.groupBy];
_.forEach(scope.items, function (item) {
scope.handlers.getGroups(scope.groupList, item, scope.items, scope.groupBy, actions);
});
_.forEach(scope.groupList, function (group) {
var items = scope.groupedItems[group.id];
items = _.sortBy(items, function (item) {
return item.description;
});
scope.groupedItems[group.id] = items;
});
var groupsToSort = _.where(scope.groupList, {unassigned: false});
var unassignedGroups = _.sortBy(_.where(scope.groupList, {unassigned: true}), 'name');
scope.groupList = _.sortBy(groupsToSort, function (group) {
return group.name;
});
//adds unassigned groups to a new array via the javascript "push" method
if (angular.isDefined(unassignedGroups)) {
for (var i = 0; i < unassignedGroups.length; i++) {
scope.groupList.push(unassignedGroups[i]);
}
}
});
scope.handlers = {
getGroups: function (groupList, item, items, groupBy, actions) {
var group = item[groupBy];
if (_.isEmpty(group)) {
scope.handlers.addGroupToList(groupList, APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE, items, groupBy, item, actions, scope);
}
else {
if (angular.isArray(group) || angular.isObject(group)) {
_.forEach(group, function (groupName) {
if (groupName == APP_CONSTANTS.ZERO) {
scope.handlers.addGroupToList(groupList, APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE, items, groupBy, item, actions, scope);
return;
}
scope.handlers.addGroupToList(groupList, groupName, items, groupBy, item, actions, scope);
})
} else {
scope.handlers.addGroupToList(groupList, group, items, groupBy, item, actions, scope);
}
}
},
addGroupToList: function (groupList, groupId, items, groupBy, item, handlers, scope) {
var id = _.camelCase(groupId);
var group = _.find(groupList, {id: id});
//var group = _.forEach(groupList, {id: id})
//return {id};
if (!group) {
var name = '';
var unassigned = false;
var link = null;
if (groupId == APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE || groupId == APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE_PARENT_ID) {
if (groupId == APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE_PARENT_ID) {
name = APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.NONE;
} else {
name = APP_CONSTANTS.DEVICE_NONE_NAME_MAPPING.UNASSIGNED;
}
unassigned = true;
} else {
link = handlers.getGroupLink(groupId);
name = handlers.getGroupName(groupId, items);
}
group = {id: id, name: name, unassigned: unassigned, link: link};
groupList.push(group);
}
scope.groupedItems[group.id] = scope.groupedItems[group.id] || [];
if (angular.isDefined(handlers.processingGroup)) {
handlers.processingGroup(group, groupList, groupId, items, groupBy, item, handlers, scope);
} else {
scope.groupedItems[group.id].push({
description: handlers.getItemDescription(item),
link: handlers.getItemLink(item)
})
}
}
};
}
};
}]);
});
You can just use filter:
var group = groupList.filter((group) => group.id === id);
EDIT: to return only the element, and not an array, when there is only one match, you can do the following:
var checkSingle = (groups) => groups.length === 1 ? groups[0] : groups;
var group = checkSingle(groupList.filter((group) => group.id === id));
You can _(groupList).groupBy('id').get(id):
var groupList = [
{ id: 1, name: 'site' },
{ id: 2, name: 'test' },
{ id: 2, name: 'prod' },
{ id: 3, name: 'dev' },
{ id: 4, name: 'back' },
{ id: 4, name: 'front' },
{ id: 5, name: 'sprint' }
];
console.log(_(groupList).groupBy('id').get(2));
console.log(_(groupList).groupBy('id').get(3));
console.log(_(groupList).groupBy('id').get(4));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>

How do I deal with an object sent into AngularJS controller?

In my controller I have a function that recieves an object from Java controller. My AngularJS variable is simple:
var self = this;
self.item = {};
And my function where I get the object:
function getItem() {
MyService.getItem(REST_SERVICE_URI)
.then(
function (d) {
self.item = d;
},
function (errResponse) {
console.error('Error while getting item');
}
);
}
Object that's received has rather complicated structure. It has id, name and list of child objects, who have also id and name fields. How do I get into this object's fields and list in the AngularJS controller? I tried loop though list using fucntion below to even count duplicate values but it didn't work. I tried even to include one more loop into it with outputing result in console, no effect. It only returns zeros.
var i = "SOME TEST NAME VALUE TO CHECK";
function getCount(i) {
var iCount = iCount || 0;
for (var el in self.item) {
console.log("let me see what are you: " + el);
if (el == i) {
iCount++;
}
}
return iCount;
}
The object I recieve is ok, I can see it content in Chrome using F12 - Network - Response or Preview.
added later:
On my page I test it like this
<tr class="my_item" ng-repeat="p in ctrl.item.children">
<span>getCount {{p.name}}: {{ctrl.getCount(p.name)}}</span>
</tr>
It displays p.name in the span btw. Java object structure is
public class Item {
private int id;
private String name;
List<Child> children = new ArrayList<>();
}
Child class is simple
public class Child {
private int id;
private String name;
}
As per your question, the content is complex and has recursive properties inside child content.
So you need to iterate on content recursively, inside one forEach loop.
See this example working Demo:
var myApp = angular.module('myApp', []);
myApp.controller('ExampleController', function() {
var vm = this;
vm.count = 0;
vm.searchTxt = "";
vm.getCount = function() {
vm.count = 0; //clear count before search
recursive(vm.content);
}
function recursive(dataArray) { //recursive function
dataArray.forEach(function(data) {
if (vm.searchTxt == data.name) { //match name property
vm.count = vm.count + 1;
}
if (data.child.length > 0) {
recursive(data.child); // call recursive function
}
});
}
vm.content = [{ //example content
id: 1,
name: 'one',
child: [{
id: 1.1,
name: 'new one',
child: [{
id: 1,
name: 'one',
child: []
}]
}]
}, {
id: 2,
name: 'two',
child: [{
id: 1.1,
name: 'new two',
child: []
}]
}]
});
<script src="https://code.angularjs.org/1.5.2/angular.js"></script>
<div ng-app="myApp" ng-controller="ExampleController as vm">
<input ng-model="vm.searchTxt" placeholder="ender search.." />
<br>
<button ng-click="vm.getCount()">Search</button>
<br>
<span>Match 'Name' count : {{vm.count}}</span>
</div>

Get selected option from backend using Angularjs

How can i init my select with option, that selects on backend (Symfony). Now i init my select (ng-model="myselect" example) like ng-init="myselect='0'"
I need to set actual selected option in ng-init, but if i remove ng-init directive it creates empty option with value ? undefined:undefined ?
What should i do?
<div ng-app="myapp">
<fieldset ng-controller="FirstCtrl">
<select
ng-options="p.id as p.first + ' ' + p.last for p in people"
ng-model="selectedPerson"></select>
{{ selectedPerson }}
</fieldset>
var myapp = angular.module('myapp', []);
myapp.controller('FirstCtrl', function ($scope) {
$scope.selectedPerson = 2;
$scope.people = [
{ id: 1, first: 'John', last: 'Rambo', actor: 'Silvester' },
{ id: 2, first: 'Rocky', last: 'Balboa', actor: 'Silvester' },
{ id: 3, first: 'John', last: 'Kimble', actor: 'Arnold' },
{ id: 4, first: 'Ben', last: 'Richards', actor: 'Arnold' }
];
});
This is what you expected..
Also check with this link http://jsfiddle.net/kaehejgo/
I was able to win this problem.
Anonymous function that turn vanilla html selects into angular models using my own function that converts string into object reference.
(function(){
window.selectControl = [];
var selects = document.querySelectorAll(".filter__select_range");
selects.forEach( function(elem, index) {
var _elem = angular.element(elem);
if (elem.querySelector("[selected]") !== undefined) {
window.selectControl[_elem.attr("data-model")] = elem.querySelector("[selected]").value;
_elem.attr({
"ng-model": _elem.attr("data-model"),
"ng-change": _elem.attr("data-change"),
"ng-init": _elem.attr("data-model") + "='"+ _elem.find('option:selected').val() +"'",
});
_elem.removeAttr('data-model').removeAttr('data-change');
stringToPath($scope, _elem.attr('ng-model'), elem.querySelector("[selected]").value);
}
});
})();
And stringToPath function
function stringToPath(obj, str, val) {
str = str.split(".");
for (var i = 0; i < str.length; i++) {
if (i != str.length-1 && str[i] != '') {
if (typeof obj[str[i]] !== "object") {
obj[str[i]] = {}
}
stringToPath(obj[str[i]], str.splice(1).join("."), val);
}
else {
obj[str[i]] = val;
}
}
return obj;
}
And one of my select in twig syntax
{{- form_widget(form, {'attr': attr|merge({'class': 'filter__select filter__select_range', 'data-model': 'FilterCollection.range.area.min', 'data-change': "checkSelectedFilters();"})}) -}}

Mithril - how to populate drop down list of view from API

I'm trying to populate a drop down box rendered by Mithril's view from methods being called outside of its module (not sure if this terminology is correct, but outside of the property which contains the view, model and controller).
This Chrome extension adds a new field to an existing page and depending on what the user select, the drop down box should refresh to items pertaining to the selected item. I can get up to the stage of getting the new list of items, but i cannot get the drop down list to redraw with the new objects.
The following shows the module which gets inserted inside an existing page:
var ItemsList = {
model: function () {
this.list = function (id) {
var d = m.deferred()
// Calls Chrome extension bg page for retrieval of items.
chromeExt.getItems(pId, function (items) {
// Set default values initially when the controller is called.
if (items.length === 0) {
items = [
{name: 'None', value: 'none'}
]
}
d.resolve(items || [])
})
return d.promise
}
},
controller: function () {
this.model = new ItemsList.model()
this.index = m.prop(0)
this.onchange = function (e) {
console.info('ctrl:onchange', e.target)
}
// Initialise the drop down list array list.
this.dropDownItemsList = m.prop([]);
// This sets the default value of the drop down list to nothing by calling the function in the model,
// until the user selects an item which should populate the drop down list with some values.
this.getItems = function(pId) {
this.model.list(pId).then(function (data) {
this.dropDownItemsList(data)
m.redraw()
}.bind(this))
}
this.getItems(0);
},
view: function (ctrl) {
var SELECT_ID = 'record_select'
return vm.Type() ? m('div', [
m('.form__item', [
m('.label', [
m('label', {
htmlFor: SELECT_ID
}, 'ID')
]),
m('.field', [
m('select#' + SELECT_ID, {
onchange: ctrl.onchange.bind(ctrl)
},
ctrl.dropDownItemsList().map(function (it, i) {
return m('option', {
value: it.value,
checked: ctrl.model.index === i
}, it.name)
})
),
])
]),
]) : null
}
}
And it is mounted using
m.mount("element name here", ItemsList);
The code which checks to see if the item has changed is using a mutation observer, and whenever it detects changes to a certain field, it will call a method to get the new values. I can see that the return value has my new items.
I have tried various different methods on trying to update the drop down list, first by trying to set the "this.list" with the new items list i've got, or trying to create a returnable method on the controller which i can call when the mutation observer fires.
After getting the new items, how can i make the drop down list show the new items which has been retrieved?
I have read guides which shows functions in the controller or model being run - but only if they've been defined to use them already in the view (i.e. have an onclick method on the view which calls the method) but so far i cannot figure out how to update or call methods from outside of the module.
Is there a way to achieve the above or a different method i should approach this?
After some more research into how Mithril works, seems like that it's not possible to call any functions defined within the component.
Due to this, i have moved the model outside of the component (so now it only has the controller and the view defined) and bound the view to use the model outside of the component.
Now calling a function which updates the model (which is now accessible from elsewhere in the code) and redrawing shows the correct values that i need.
If I understand correctly, you need to have two variables to store your lists, one to store the old list and one to store the updated list so you can always map the updated one and go to your old one if you need.
Here is a simple implementation of a drop down list with some methods to update and search. You can update the list on the fly using the methods.
mithDropDown
jsFiddle
var MythDropDown = function(list) {
if (Array.isArray(list))
this.list = list;
else
list = [];
if (!(this instanceof MythDropDown))
return new MythDropDown(list);
var self = this;
this.selected = {
name: list[0],
index: 0
};
this.list = list;
};
MythDropDown.prototype.view = function(ctrl) {
var self = this;
return m('select', {
config: function(selectElement, isinit) {
if (isinit)
return;
self.selectElement = selectElement;
self.update(self.list);
},
onchange: function(e) {
self.selected.name = e.target.value;
self.selected.index = e.target.selectedIndex;
}
},
this.list.map(function(name, i) {
return m('option', name);
}));
};
MythDropDown.prototype.getSelected = function() {
return (this.selected);
};
MythDropDown.prototype.update = function(newList) {
this.list = newList;
this.selectElement.selectedIndex = 0;
this.selected.name = newList[0];
this.selected.index = 0;
};
MythDropDown.prototype.sort = function() {
this.list.sort();
this.update(this.list);
};
MythDropDown.prototype.delete = function() {
this.list.splice(this.selected.index, 1);
this.update(this.list);
};
var list = ['test option 1', 'test option 2'];
var myList = new MythDropDown(list);
var main = {
view: function() {
return m('.content',
m('button', {
onclick: function() {
var L1 = ['Banana', 'Apple', 'Orange', 'Kiwi'];
myList.update(L1);
}
},
'Fruits'),
m('button', {
onclick: function() {
var L1 = ['Yellow', 'Black', 'Orange', 'Brown', 'Red'];
myList.update(L1);
}
},
'Colors'),
m('button', {
onclick: function() {
myList.sort();
}
},
'Sort'),
m('button', {
onclick: function() {
myList.delete();
}
},
'Remove Selected'),
m('', m.component(myList),
m('', 'Selected Item: ' + myList.selected.name, 'Selected Index: ' + myList.selected.index)
)
);
}
};
m.mount(document.body, main);
<script src="https://cdnjs.cloudflare.com/ajax/libs/mithril/0.2.3/mithril.min.js"></script>

Categories

Resources