Handle multiple radio buttons in a tree view with React - reactjs

I have a dataset with menu items and nested sub-menu items, coming from a GET request. With that, I render a table in a react-hook-form for assigning access permissions on each menu item.
[
{
moduleId: 2,
moduleName: "Menu 1",
permission: 0,
subModuleList: [
{
moduleId: 7,
moduleName: "Menu 1.1",
permission: 0,
subModuleList: null
},
{
moduleId: 8,
moduleName: "Menu 1.2",
permission: 0,
subModuleList: [
{
moduleId: 11,
moduleName: "Menu 1.2.1",
permission: 0,
subModuleList: null
},
{
moduleId: 33,
moduleName: "Menu 1.2.2",
permission: 0,
subModuleList: null
},
{
moduleId: 49,
moduleName: "Menu 1.2.3",
permission: 0,
subModuleList: null
},
{
moduleId: 68,
moduleName: "Menu 1.2.4",
permission: 0,
subModuleList: null
}
]
}
]
}
]
Each table row is a menu item and has 2 radio buttons (no access & read) if it has children, and 3 radio buttons (no access, read & read/write) if is without children.
What I am trying to accomplish, is that each time a radio button is clicked on a child menu item, I want all the parent menu items to update as well with a relevant checked radio button. I feel really stacked on how to achieve this with react.
Here is a code sandbox link with all the current implementations.
Example use case: When checking the menu item Menu 1.2.2 with read or read/write permissions, the Menu 1.2 and Menu 1 should also have checked the read permission
Thank you in advance for your time!

So after some thought, I came up with a solution. The first step is to identify all the parents of each child is clicked.
function getPath(object, search) {
if (object.moduleId === search) return [object.moduleName];
else if (object.subModuleList || Array.isArray(object)) {
let children = Array.isArray(object) ? object : object.subModuleList;
for (let child of children) {
let result = getPath(child, search);
if (result) {
if (object.moduleId) result.unshift(object.moduleName);
return result;
}
}
}
}
This function returns an array of names of all the parent elements. Then I had to find these parent elements on my dataset and update their values based on my needs. There is also a check if the parent element has children, and one or more are checked with value > 0, to retain the parent as checked.
//get all parent objects
const findId = (object, key, searchTerm, newValue) => {
if (object === null) return;
if (Array.isArray(object)) {
for (const obj of object) {
findId(obj, key, searchTerm, newValue);
}
} else {
if (object.hasOwnProperty(key) && object[key] === searchTerm) {
if (object.subModuleList !== null) {
if (newValue >= "1") newValue = "1";
if (newValue === "0") {
const childActive = object.subModuleList.some(
(element) => element.permission !== "0"
);
if (childActive) {
newValue = "1";
} else {
newValue = "0";
}
}
}
object.permission = newValue;
return object;
}
for (const k of Object.keys(object)) {
if (typeof object[k] === "object") {
findId(object[k], key, searchTerm, newValue);
}
}
}
};
Here you can find an updated code sandbox
I hope that someone will benefit from this solution. Also, I want to mention that the code presented here, is also from answers of other devs on other topics.

Related

AngularJS filter already selected option from dynamic field

I have a form where you can add x number of fields. Each field contains option select. I want to filter out the already chosen option when this option is already chosen in one or multiples field before. Each field has a remove button and the form has 1 add button.
How can I filter out the dynamic fields?
Any help,guidance is most welcome.Thanks in advance. :)
This is how my HTML looks like:
<div data-ng-repeat="choice in choices">
<select data-ng-model="choice.option"
data-ng-options="item as item.Value for item in options">
</select>
<button data-ng-click="removeChoice(choice)">Remove choice</button>
<div>
<button data-ng-show="choices.length <= 4" data-ng-click="addNewChoice()">Add Choice</button>
</div>
</div>
And my controller:
$scope.options = [
{
"Key": "0",
"Value": "Select an option"
},
{
"Key": "Option1",
"Value": "Option1"
},
{
"Key": "Option2",
"Value": "Option2"
},
{
"Key": "Option3",
"Value": "Option3"
},
{
"Key": "Option4",
"Value": "Option4"
},
{
"Key": "Option5",
"Value": "Option5"
}
];
$scope.choices = [{ id: '1' }];
$scope.addNewChoice = function () {
var newItemNo = $scope.choices.length + 1;
$scope.choices.push({ id: newItemNo, option: $scope.option, value: $scope.value });
};
$scope.removeChoice = function () {
var index = $scope.choices.indexOf(choice);
$scope.choices.splice(index, 1);
};
ok
i can give simple recommendation which will be this.
1: add variable $scope.selectedOptions = [];
this will contain list of already selected options from all select elements .
2: create function $scope.AddSelectedOption(item);
this will add the selected object when we change option from any select element because we are going to use for all selects ng-change= "AddSelectedOption(item);"
3: add checkIfSelected(item); this will check if given object value is already selected or not ..
will user in
hope you understand what it will do just check like this
$scope.checkIfSelected = function (item) {
$scope.selectedFound = $scope.selectedOptions.filter(function
(option) {
if(option.value == item.value)
{
return day;
}
});
if($scope.selectedFound.length == 0 ) { return false; } else {
return true; }
}
This will return true if give item found in the options.
if not out.. you can invite me to help again .
This is possible. I'm explaining a basic version of this requirement. See the working example here http://plnkr.co/edit/S9yZpjhY55lXsuifnUAc?p=preview
What wer are doing is maintaining another options which is the copy of the original options. Copying the options will make it to not reference existing options since objects are pass by reference in Javascript.
The main logic is in this function, which modify the options on selection:
$scope.optionSelected = function(choice) {
$scope.availableOptions = $scope.availableOptions || angular.copy($scope.options);
if (choice.option) {
var index = -1;
// See if available options has that key
angular.forEach($scope.availableOptions, function(item, i) {
if (item.Key === choice.option.Key) {
index = i;
}
});
if (index > -1) {
// And then remove it
$scope.availableOptions.splice(index, 1);
}
}
};

Finding the deepest nested components in DOM tree

I am trying to implement a keybinding mixin which let me write something like
createClass({
...
keybindings: function() {
return {
'escape' : function(event) { this._handleEscapeKey(); },
'enter' : function(event) { this._handleEnterKey(); },
'up' : function(event) { this._handleUpKey(); },
'down' : function(event) { this._handleDownKey(); },
'left' : function(event) { this._handleLeftKey(); },
'right' : function(event) { this._handleRightKey(); },
};
},
In my component. However I have problems when multiple components include the mixin. I need a way make the event listeners on the most deeply nested components in the DOM tree get precedence over its ancestors.
This is what I got so far, any ideas/suggestions/feedback is much appreciated
mixin:
KeybindingsMixin = {
modifiers: {
shift: 16,
ctrl: 17,
alt: 18,
meta: 91
},
keyCodes: {
escape : 27,
up : 38,
down : 40,
left : 37,
right : 39,
enter : 13,
shift : 16,
ctrl : 17,
alt : 18,
meta : 91,
s : 83,
},
singleModifier: {
shift: {
keyCode : 16,
shift : true,
ctrl : false,
alt : false,
meta : false
}
},
componentDidMount: function() {
if (this.__keybindings !== undefined) {
var keybindings = this.getAllKeybindings();
this.__keybindings = _.merge(this.__keybindings, keybindings);
$(window).on('keydown', this.__matchEvent);
}
},
componentWillUnmount: function() {
if (this.__keybindings !== undefined) {
var keybindings = _.keys(this.getAllKeybindings());
this.__keybindings = _.omit(this.__keybindings, keybindings);
$(window).off('keydown', this.__matchEvent);
}
},
childContextTypes: {
__keybindings: React.PropTypes.object
},
contextTypes: {
__keybindings: React.PropTypes.object
},
getAllKeybindings: function() {
return this.__getKeybindings();
},
getChildContext: function() {
return {
__keybindings: this.__getKeybindings()
};
},
__getKeybindings: function() {
var keybindings = Object.getPrototypeOf(this).keybindings();
this.__keybindings = this.__keybindings || (this.context && this.context.__keybindings) || keybindings || {};
return this.__keybindings;
},
__parseKeybindingString: function(binding) {
var tokens = binding.split(' ');
var modifiers = _.keys(this.modifiers);
var bindings = _.keys(this.keyCodes);
var parsedEvent = {
keyCode: 0,
alt: false,
ctrl: false,
shift: false,
meta: false
};
_.each(tokens, function(token) {
if (_.includes(modifiers, token)) {
parsedEvent[token] = true;
} else if (_.includes(bindings, token)) {
parsedEvent.keyCode = this.keyCodes[token];
}
}, this);
return parsedEvent;
},
__keybindingSpecified: function(event) {
},
__matchEvent: function(event) {
var eventMatcher = {
keyCode: event.keyCode,
alt: event.altKey,
ctrl: event.ctrlKey,
shift: event.shiftKey,
meta: event.metaKey,
};
var keybindings = this.__keybindings;
_.forOwn(keybindings, function(action, binding) {
var parsedEvent = this.__parseKeybindingString(binding);
if (_.isEqual(eventMatcher, parsedEvent)) {
event.preventDefault();
return action.call(this, event);
}
}, this);
return;
},
};
If by "deeply nested components in the DOM tree get precedence over its ancestors" you mean that the event should only trigger on the most deeply nested component and not bubble up to the components further up, you should call event.stopPropagation(). This cancels the bubbling, and the components further up won't get the event.
EDIT
Ah, sorry, I thought you used Reacts event system which allows stopPropagation even if they only attach one event handler to the root element. What you can do is something similar to what React does, in that for each element that uses the the mixin, you attach some property to that DOM node to say that it's listening to the event. Then when the event comes in, you check event.target and see if it has that property. If it doesn't, you check event.target.parentElement and recursive upwards until you find it. And as soon as you find that element, you stop going upwards to simulate the effect of stopPropagation.

QML How to make QAbstractTableModel with qml tableview checkable

I’m new in QT. I want to make every row of qml tableview checkable but it doesn’t work.
The tableview with data is shown successfully but it’s not checkable.
It seems that flags() and setData() functions are never run and role==Qt::CheckStateRole never be true.
Please help.
C++ code
I modified code like but it doesn't work:
QVariant TableModel::data(const QModelIndex & index, int role) const {
if (index.row() < 0 || index.row() >= _fields->size())
return QVariant();
if(role == Qt::CheckStateRole) {
return rowsChk.contains(index.row()) ? Qt::Checked : Qt::Unchecked;
}
switch(role) {
case NameRole:
return model.name();
case DescriptionRole:
return model.description();
case TypeRole:
return model.type();
}
return QVariant();
}
bool TableModel::setData(const QModelIndex & index, const QVariant & value, int role){
rowsChecked(index.row(), value) ;
emit dataChanged(index, index);
return true;
}
here is my qml file
TableView {
model: tableModel
anchors.fill: parent
frameVisible: true
headerVisible: true
sortIndicatorVisible: false
alternatingRowColors: true
Component {
id: checkBoxDelegate
Item {
CheckBox {
anchors.fill: parent
checked: styleData.value
}
}
}
TableViewColumn {
role: "check"
title: ""
width: 30
delegate: checkBoxDelegate
}
TableViewColumn {
role: "name"
title: "Name"
width: 200
}
TableViewColumn {
role: "description"
title: "Description"
width: 100
}
TableViewColumn {
role: "type"
title: "Type"
width: 100
}
The setData() method is not called because you probably haven't reimplemented method:
QHash<int, QByteArray> roleNames() const;
which is a virtual method of QAbstractItemModel, the class that QAbstractTableModel inherits. Without this the QML does not know what your roles mean and does not care about your data. In your case this method should be defined this way:
QHash<int, QByteArray> FieldModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[DescriptionRole] = "description";
roles[FilterRole] = "filter";
roles[TypeRole] = "type";
return roles;
}
Personally, I have no idea why the
Qt::ItemFlags flags(const QModelIndex &index) const;
method is not called - have the same problem. But you can solve it by defining a delegate of TableViewColumn in your QML:
Component {
id: checkBoxDelegate
Item {
CheckBox {
anchors.fill: parent
checked: styleData.value
}
}
}
and then assign this delegate to your columns:
TableViewColumn {
role: "name"
title: "Name"
width: 200
delegate: checkBoxDelegate
}
Then the column contains a CheckBox control which value is specified by method
QVariant FieldModel::data(const QModelIndex & index, int role) const;
just return value 0 or QString("false") for unchecked and 1 or QString("true") for checked state.

Changing the name of a button in jquery-steps

I have included the jquery-steps plugin.
How can I change the buttons texts?
Now it says "finish" I want to change that into "go"
Thanks
Check out the following link. You can change all labels on initialization.
var settings = {
labels: {
current: "current step:",
pagination: "Pagination",
finish: "Finish",
next: "Next",
previous: "Previous",
loading: "Loading ..."
}
};
$("#wizard").steps(settings);`
I just needed to change button text depending on condition. And it can be done without changing settings just like that
if(true){
$('a[href$="finish"]').text('Go');
}else{
$('a[href$="finish"]').text('No Go');
}
You can do this:
form.steps({
headerTag: "h4",
bodyTag: "section",
transitionEffect: "fade",
labels:
{
finish: "Go",
},
onStepChanging: function (event, currentIndex, newIndex)
{
//change color of the Go button
$('.actions > ul > li:last-child a').css('background-color', '#f89406');
form.validate().settings.ignore = ":disabled";
return form.valid();
},
onFinishing: function (event, currentIndex)
{
form.validate().settings.ignore = ":disabled";
return form.valid();
},
onFinished: function (event, currentIndex)
{
form.submit();
}
});
If the label / text of the buttons should change dynamically depending on the language, you can use this:
/* dynamic change prev-next button text language (check lang attribute in html tag) */
var language = $('html').attr('lang');
$(window).on('load', function () {
if(language != 'de'){
$('a[href$="next"]').text('Next');
} else {
$('a[href$="next"]').text('Weiter');
}
});

Using object property in select

I currently have the following select:
<select ng-model="current_event"
ng-options="event.event_id for event in current_level.events.event"
ng-change="currentEventChanged()">
</select>
current_level.events.event is an array of objects which looks like this:
[
{
"event_id": 0,
"event_type": {
"collision": {
"object_id1": 0,
"object_id2": 1,
"allow_overlap": "no"
}
}
},
{
"event_id": 1,
"event_type": {
"player_input": {
"object_id": 0,
"level_id": 0,
"allow_overlap": "no"
}
}
}
]
The select works perfectly, but the text for each option is just the event_id, e.g. "0" or "1". I want the text for an item to actually be the event_type (with underscores converted to spaces), e.g. "collision" or "player input". Is this possible in AngularJS?
You're using event.event_id, so there is nothing wrong in the behaviour of AngularJS. You should simply put the right label in the ng-options of <select> (see the documentation).
However, you need to call a function before in order to obtain the correct label, since what you want is pretty complex.
$scope.getLabel = function (eventType)
{
var firstKey = null;
angular.forEach(eventType, function (value, key)
{
if (firstKey === null)
{
firstKey = key;
}
});
return firstKey.replace('_', ' ', 'g');
};
<select
ng-model="current_event"
ng-options="event.event_id as getLabel(event.event_type) for event in current_level.events.event"
ng-change="currentEventChanged()"
>
</select>
jsFiddle

Resources