I have a table that where the data is periodically updated by a javascript interval function in my controller:
var model = this.getview().getModel();
var updateModel = setInterval(function(){
model.loadData('path/to/my/data.json');
}, 30000)
This will basically be static display on a public monitor showing a summary of data.
I want to be able to highlight when a property has changed, so I've been trying to add a class to the control when it changes. The class will then highlight this in some way with CSS.
<Table items="{items}">
<columns>
<Column/>
<Column/>
</columns>
<items>
<ColumnListItem>
<cells>
<Text
text="{name}" />
<ObjectStatus
text="{value}"
state="{
path: 'value',
formatter: '.formatter.redOrGreen'
}"/>
</cells>
</ColumnListItem>
</items>
</Table>
So the model updates every 30 seconds. If the {value} field changes, I want to add a class to ObjectStatus control.
At the moment I'm just using a JSON model for local development to see if this is possible, but in production it will be an oData service.
Thanks for the answers, I managed to solve this, but my method wasn't quite covered by the answers on here. This is how I did it:
The requirements for this changed slightly since I posted the question. I'll need to indicate if something has changed, but also if the value has gone up or down. I'll also need to indicate if something goes above or below a certain value. I also wanted to make a solution that could be easily adapted if there are any other future requirements. This will also need to be easily adapted for oData when the backend service is up and running.
First of all (and key to this) is setting up a duplicate model, so this goes into my component.js file .I'm just duplicating the model here so that the values old and new values are unchanged, to make the formatter functions work on the first page load:
var oModel = new JSONModel('/path/to/data.js');
this.setModel(oModel, 'model');
this.setModel(oModel, 'oldModel');
In the controller for my view, I then take a copy of the old data, which goes into the old model that I've attached to the view, the new model is then updated. I do this in the after rendering hook to optimize the initial page load.
onAfterRendering: function(){
var thisView = this.getView();
var updateModel = function(){
var oldData = thisView.getModel('model').getData();
var oldModel = new JSONModel(oldWharehousesData);
thisView.setModel(ollModel, 'oldModel');
//update model
var newModel = thisView.getModel('model');
model.loadData('/path/to/data.js');
};
window.refershInterval = setInterval(updateModel, 30000);
}
I'm then able to input the new and old values to a formatter in my XML view and output a couple of custom data attribute:
<core:CustomData
key="alert-status"
value="{
parts: [
'model>Path/To/My/Property',
'oldModel>Path/To/My/Property'
],
formatter: '.formatter.alertStatus'
}"
writeToDom="true"/>
</customData>
My formatter.js :
alertStatus: function(newValue, oldValue){
var alertNum = 25;
if(newValue < alertNum && oldValue >= alertNum) {
return 'red';
} else if (newValue >= alertNum && oldValue < alertNum) {
return 'green';
} else {
return 'none';
}
}
I can then have as many custom data attributes as I like, run them through their own formatter function, which can be styled to my heart's content, e.g:
compareValues: function(newValue, oldValue) {
if (newValue > oldValue) {
return 'higher';
} else if (newValue < oldValue){
return 'lower';
} else {
return 'false';
}
}
I have build an example on JSBin.
First you have to get the received data. You can use the
Model.attachRequestCompleted event for that:
this.model = new sap.ui.model.json.JSONModel();
this.model.attachRequestCompleted(this.onDataLoaded, this);
In the event handler onDataLoaded you can retrieve the JavaScript object and compare it to a saved copy. You have to write the flags that indicate changes to the array item itself. (Storing it in a separate model as Marc suggested in his comment would not work because in your aggregation binding you only have the one context to your array item.)
At last you have to save the newData object as this.oldData for the next request.
onDataLoaded:function(){
var newData = this.model.getProperty("/");
if (this.oldData){
//diff. You should customize this to your needs.
for(var i = 0, length = Math.min(newData.items.length, this.oldData.items.length); i< length; i++){
newData.items[i].valueChanged = newData.items[i].value !== this.oldData.items[i].value;
newData.items[i].nameChanged = newData.items[i].name !== this.oldData.items[i].name;
}
}
this.oldData = newData;
this.getView().getModel().setProperty("/",newData);
},
You can then bind the ObjectState state property to the flag(s):
<ObjectStatus
text="{value}"
state="{= ${valueChanged} ? 'Warning':'None' }"/>
If you want to change the background color of the whole row or something like that you can apply Bernard's answer and use the flag(s) in a customData attribute.
You can use the <customData> tag
This allows the insertion of a custom attribute into the HTML produced by the XML to HTML conversion process
In the example below for example I add a custom attribute (my own) - this code generates the following attribute data-colour in a relevant HTML element (a <SPAN> tag) - inspect the relevant element using, say, Chrome.
<customData>
<core:CustomData writeToDom="true" key="colour" value="{vproducts>ListCostColour}" />
</customData>
You are then able to create a style for this attribute in your own style sheet as follows (and reference this in your manifest.json)
[data-colour="red"] {
background-color: #ffd1cc;
}
[data-colour="orange"] {
background-color: rgba(255, 243, 184, 0.64);
}
[data-colour="green"] {`enter code here`
background-color: rgba(204, 255, 198, 0.97);
}
Related
I am learning SAPUI5 at the moment and created a little application for myself to play around and learn by practice. Before I get to my question, I will provide some short information about my app, so you know what I am trying to do and what should be the expected result.
What does the app?
The only thing this application does is providing a calendar and a table for the user. If the user clicks on a date, then the table should be filled with this date and two time pickers to give a start and end time for this selected day. For every date a new row should be created.
The actual problem:
The problem is a design problem I guess. The table gets filled with the dates and the time pickers, but my way of doing this is, is bad practice I guess. I store my selected dates in two models, one for the calendar, one copy to bind it to the table. That works and dates are saved and I can access the data via model. When a new dates get added to the table, the two timepickers get added too because they are provided in the ColumnListItem in the XML view, but no model is bound to them nor do I have access to the values of the timepickers e.g. via ID. And here is the problem, until now I found no proper way of how to them bind to a model or another clean way nor to access the values of the created timepickers.
The question:
How would you implement this in a clean way, so that the time pickers are bound in a right way maybe to a model or models? and you can access their data? I would be thankful if you could give me an advice or hint how I should implement this in a clean way since I want to learn from this and don't want to start hacking around with bad practices just to achieve the goal in shorter time.
The relevant sourcecode:
Controller:
var CalendarController = Controller.extend("sap.ui.unified.sample.CalendarMultipleDaySelection.CalendarMultipleDaySelection", {
oFormatYyyymmdd: null,
oModel: null,
onInit: function(oEvt) {
this.oFormatYyyymmdd = sap.ui.core.format.DateFormat.getInstance({
pattern: "dd.MM.yyyy",
calendarType: sap.ui.core.CalendarType.Gregorian
});
this.oModel = new JSONModel({
selectedDates: []
});
this.oCopyModel = new JSONModel({
selectedDates: []
});
var oCalendar = this.getView().byId("calendar");
oCalendar.setModel(this.oModel);
},
handleCalendarSelect: function(oEvt) {
var oCalendar = oEvt.oSource;
var aSelectedDates = oCalendar.getSelectedDates();
console.log(aSelectedDates);
var oDate;
var oData = {
selectedDates: []
};
var oTable = this.getView().byId("dateTable");
if (aSelectedDates.length > 0) {
for (var i = 0; i < aSelectedDates.length; i++) {
oDate = aSelectedDates[i].getStartDate();
oData.selectedDates.push({
Date: this.oFormatYyyymmdd.format(oDate)
});
}
this.oModel.setData(oData);
if (this.oCopyModel.getProperty("/selectedDates/length") >= 0) {
this.oCopyModel.setData(oData);
oTable.setModel(this.oCopyModel);
}
} else {
this._clearModel();
}
},
return CalendarController;
View:
<content>
<unified:Calendar id="calendar" select="handleCalendarSelect" intervalSelection="false" singleSelection="false"/>
<Table id="dateTable" items="{path: '/selectedDates', sorter: {path: 'Date', comparator: '.dateComperator'}}"mode="None" fixedLayout="true">
<columns>
<Column>
<header>
<Text text="Date"/>
</header>
</Column>
<Column>
<header>
<Text text="Beginning"/>
</header>
</Column>
<Column>
<header>
<Text text="End"/>
</header>
</Column>
</columns>
<ColumnListItem>
<Text text="{Date}"/>
<TimePicker value="10:00" valueFormat="HH:mm" displayFormat="HH:mm" change="handleChange"/>
<TimePicker value="11:00" valueFormat="HH:mm" displayFormat="HH:mm" change="handleChange"/>
</ColumnListItem>
</Table>
Kind regards
Maximilian
I created a small example:
https://next.plnkr.co/edit/OGmJimjF2YZ46mv6DsF2?preview
A few points:
I simply added a few properties (startTime and endTime) to a selected date. You can now modify the time with the timepicker, the changes are stored in the model.
The data binding of the calender seems broken. I also had to use getSelectedDates. This may be due to singleSelection="false". When using single selection you can access the selected date (or interval) via data binding.
Never access internal properties (oEvt.oSource). There are accessors for this (oEvt.getSource()).
I'm looking for a convenient method to get the path from a table cell.
Background: It is required to implement a search field allowing to filter on all columns of responsive table. Here, the path is needed as parameter for the filter object.
XML Code
<Table items="{path: 'modelName>pathPart1/pathPart2'}">
<headerToolbar>
<Toolbar>
<Title text="titleText"/>
<SearchField search="searchInTable"/>
</Toolbar>
</headerToolbar>
<columns>
<Column>
<Text text="column1"/>
</Column>
<Column>
<Text text="column2"/>
</Column>
</columns>
<ColumnListItem>
<Text text="{modelName>cellName1}"/>
<Text text="{modelName>cellName2}"/>
</ColumnListItem>
</Table>
Controller Logic
searchInTable: function(event) {
var table = event.getSource().getParent().getParent();
var query = event.getParameters("query");
table.getBinding("items").filter(this.getFilters(table, query));
},
getFilters: function(table, query) {
var aFilters = [];
var items = table.getItems();
// Loop through items aggregation and populate filter object
jQuery.each(items, function(i, oItem) {
// Get path from cells (e.g. cellName1)
var sPath = oItem.mAggregations.cells[i].mBindingInfos.text.binding.sPath;
var sOperator = FilterOperator.EQ;
var sValue1 = query;
var oFilter = new Filter(sPath, sOperator, sValue1);
aFilters.push(oFilter);
});
return aFilters;
},
Can we replace this part by a more convenient and robust method?
var sPath = oItem.mAggregations.cells[i].mBindingInfos.text.binding.sPath;
As you notice, I'm trying to receive the sPath going through the whole object. However, its not working in all cases as the structure of the object may change. I bet there is an better approach available. However, I struggling a bit here.
Any ideas?
Edit: I do like to get the path pointing to the text property in the table. In this samplle it would be: cellName2
I'm on the phone right now, so I can't test it, but it is something like this
oItem.getCells()[i].getBindingContext().getPath()
getCells() comes from the ColumnListItem API if I am not wrong.
The other two from the ODataListBinding API or something like that...
If you dive a bit in the API you will find it
EDIT: I think you should provide the model name when getting the context. But I don't remember well...
oItem.getCells()[i].getBindingContext("modelName").getPath()
Try both, with and without it...
EDIT2: Here you have the snippet http://jsbin.com/votaxiyedi/edit?html,output
And this what you need:
oItem.getBindingContext("odata").getPath() + "/" + oItem.getCells()[0].getBinding("text").getPath();
I am trying to implement List in material-ui. And i am trying to display randomly generated array of elements, they are being displayed with scrollbar list as i want. But i want to select that particular List Item which is the issue as i m unable to select it, even by using on click. So can anyone help me in this.
Here is my code:
var MuiListElement = React.createClass(
{
handleClick() {
console.log("secondList clicked")
},
render()
{
let faker = require('faker')
let myItems = []
for(let i = 0; i < 5000; i++)
{
let name = faker.Name.findName()
myItems.push(<ListItem onClick={this.handleClick()} key={i.toString()}>{name}</ListItem>)
}
return(
<div style={{width:'400px'}}>
<Paper style={{maxHeight: 200, overflow: 'auto'}}>
<List selectable='true'>
{myItems}
</List>
</Paper>
</div>
)
}
}
)
this.handleClick() calls handleClick function during your render, you should call it when the user clicks the list item. You wan't to pass the function to onClick, not call the function.
myItems.push(
<ListItem
onClick={this.handleClick} //pass function, don't call function
key={i}> //note: you can pass key as number
{name}
</ListItem>)
Now let me predict the future, you would like to know what item was clicked. You can use bind.
myItems.push(
<ListItem
onClick={this.handleClick.bind(this, i)} //bind returns a function
key={i}>
{name}
</ListItem>)
now you know what was passed
handleClick(i) {
console.log("secondList clicked", i)
},
However, every time you render, you are creating a new function and will slightly degrade performance.
See this discussion,
React js onClick can't pass value to method
Let me know if you don't understand.
I just started using knockout js. I am creating group of images using foreach binding of knockout js. Initially all the images have same css class "imageUnvisited" and I am trying to change and set the class(permanently) of the particular image that I click to show the visited state. I am successful in changing the class on click event but as soon as I click on another image, the new added class of the previously clicked image gets remove. I am new it the IT filed so pardon me if you find anything wrong, please help.
Below is the code that I am using:
var vm = {
item: jsonData.items,
clickedImageIndex: ko.observable('')
}
ko.applyBindings(vm);
function getVisitedClass(data, index) {
if (index() == vm.clickedImageIndex()) {
return "imageVisited"
}
}
function imageClicked(data, e) {
var itemTarget = e.target || e.srcElement;
index = ko.contextFor(itemTarget).$index();
vm.clickedImageIndex(index);
}
.imageUnvisited {
border: solid 1px green;
}
.imageVisited {
border: solid 1px black;
}
<div data-bind="foreach: item" id="image_gallery">
<div id="image_wrapper">
<image data-bind="attr: { id: 'image' + $index(), src: $data.Src, class: getVisitedClass($data, $index)},click: imageClicked" class="imageUnvisited" role="button"></image>
</div>
</div>
Why your current code doesn't work:
You're storing one index in clickedImageIndex which is updated on every click. Therefore, the expression index() == vm.clickedImageIndex() can only be true for one image at a time.
A "quick fix":
Instead of storing an index in clickedImageIndex, you can store multiple inside an object.
In your vm:
clickedImageIndex: ko.observable({})
In your handler:
var clicked = vm.clickedImageIndex();
clicked[index] = true;
vm.clickedImageIndex(clicked);
In your getVisitedClass:
if (vm.clickedImageIndex()[index()]) {
return "imageVisited"
}
A better fix:
It's probably a good idea to follow knockouts MVVM architecture a bit stricter... This means:
Map your images to ImageViewModel instances
Add a clicked observable property to ImageViewModel
Add a ImageViewModel.prototype.onClick method that sets this.clicked(true)
Use the data-bind="click: onClick, css: { 'imageVisited': clicked }" data-bind to update state.
I don't want to use an <input type=submit /> button to submit a form and I am instead using an <a> element. This is due to styling requirements. So I have this code:
myButton.addEvent('click', function() {
document.id('myForm').submit();
});
However, I have also written a class that improves and implements the placeholder attribute on inputs and textareas:
var FDPlaceholderText = new Class({
Implements: Events,
initialize: function() {
var _self = this;
var forms = document.getElements('form');
forms.each(function(form) { // All forms
var performInit = false;
var i = 0;
var ph = [];
form.getElements('input, textarea').each(function(el) { // Get form inputs and textareas
if (el.getProperty('placeholder') != null) { // Check for placeholder attribute
performInit = true;
ph[i] = _self.initPlaceholder(el); // Assign the placeholder replacement to the elements
}
i ++;
});
if (performInit) {
_self.clearOnSubmit(form, ph);
}
});
},
clearOnSubmit: function(form, ph) {
form.addEvent('submit', function(e) {
ph.each(function(el) {
if (el.value == el.defaultValue) {
el.value = '';
}
});
});
},
initPlaceholder: function(el) {
el.defaultValue = el.getProperty('placeholder');
el.value = el.getProperty('placeholder');
el.addEvents({
'focus': function() {
if (el.value == el.defaultValue) el.value = '';
},
'blur': function() {
if(el.value.clean() == ''){
el.value = el.defaultValue;
}
}
});
return el;
}
});
window.addEvent('domready', function() {
new FDPlaceholderText();
});
The above class works great if a form is submitted using an actual <input type=submit /> button: it listens for a submit and clears the inputs values if they are still the default ones therefore validating that they are essentially empty.
However, it seems that because I am submitting one of my forms by listening to a click event on an <a> tag the form.addEvent('submit', function(e) { isn't getting fired.
Any help is appreciated.
well you can change the click handler to fireEvent() instead of call the .submit() directly:
myButton.addEvent('click', function() {
document.id('myForm').fireEvent('submit');
});
keep in mind a couple of things (or more).
placeholder values to elements that lack placeholder= attribute is pointless
if you detect placeholder support, do so once and not on every element, it won't change suddenly midway through the loop. you can go something like var supportsPlaceholder = !!('placeholder' in document.createElement('input')); - remember, there is no need to do anything if the browser supports it and currently, near enough 60% do.
you can otherwise do !supportsPlaceholder && el.get('placeholder') && self.initPlaceholder(el); - which avoids checking attributes when no need
when the form is being submitted you really need to clear placeholder= values in older browser or validation for 'required' etc will fail. if validation still fails, you have to reinstate the placeholder, so you need a more flexible event pattern
avoid using direct references to object properties like el.value - use the accessors like el.get('value') instead (for 1.12 it's getProperty)
for more complex examples of how to deal with this in mootools, see my repo here: https://github.com/DimitarChristoff/mooPlaceholder
This is because the submit() method is not from MooTools but a native one.
Maybe you can use a <button type="submit"> for your styling requirements instead.