My ExtJS application displays certain UI elements depending on a boolean variable.
This boolean variable, however, is the result of calling an async function. As a result, the boolean is set to a Promise that is fulfilled, rather than true or false proper. This affects whether the UI elements are actually displayed (a Promise is not exactly a boolean, after all).
The code looks like this:
Ext.define('userDefinedComponent', {
extend: 'Ext.Container',
requires: ['someHelperFile'],
initComponent: function () {
var me = this,
var enabled = someHelperFile.someAsyncFunc() // enabled is a boolean that is returned as fulfilled Promise instead
Ext.apply(me, {
// layout and padding
items: [
{
xtype: 'internallyDefinedForm',
fieldConfigs: {
// other fields
'someFormField': {
hidden: !enabled, // depends on enabled
}
}
},
{
xtype: 'internallyDefinedGrid',
columnConfigs: {
// other columns
'someColumn': {
hidden: !enabled, // deends on enabled
}
},
}
]
})
}
})
I want the field enabled to really be a boolean rather than a Promise. In other words, I want to wait for the someAsyncFunc to run the result, before setting hidden property of the internallyDefinedForm and internallyDefinedGrid.
What are my possibilities? I was thinking of using a beforerender, like below:
Ext.define('userDefinedComponent', {
extend: 'Ext.Container',
requires: ['someHelperFile'],
initComponent: function () {
var me = this;
Ext.apply(me, {
// layout and padding
items: [
{
xtype: 'internallyDefinedForm',
fieldConfigs: {
// other fields
'someFormField': {
hidden: !me.enabled, // depends on enabled
}
}
},
{
xtype: 'internallyDefinedGrid',
columnConfigs: {
// other columns
'someColumn': {
hidden: !me.enabled, // deends on enabled
}
},
listeners: {
beforerender: function() { // this is the beforerender
me.enabled = someHelperFile.someAsyncFunc();
console.log("beforerender triggered in grid");
}
},
}
]
})
}
})
And in fact, using the beforerender for the internallyDefinedGrid only, I can see the text "beforerender triggered in grid" triggered very early. However, the fact remains that the behavior that I observe does not correspond to what I expect: although the async someAsyncFunc should return true based on the API response it gets, such that me.enabled is true, the actual UI associated with the internallyDefinedGrid behaves as if me.enabled is false instead. I observe that the column on the UI is hidden, and this is only possible when me.enabled is false, such that the column someColumn does not appear on the grid. After all, the hidden field of someColumn is set to !enabled.
I am confident that the UI for the grid behaves not like what I expect it to, so there is a problem with the async behavior. But I'm really lost as to how to set the asynchronously obtained enabled or me.enabled field adequately.
Any help is appreciated.
I would suggest to use a View Model and binding, as explained here.
Basically you define what your UI is depending on, under the data tag in the View Model (you can set the initial value here):
Ext.define('MyApp.TestViewModel', {
extend: 'Ext.app.ViewModel',
data: {
something: false,
},
}
Then you bind the visibility to this value in the view:
bind: {
hidden: '{something}'
}
or
bind: {
hidden: '{!something}'
}
You fetch the async data, and once you have the result, set the value in the View Model (this can be either the view or the controller):
this.getViewModel().set('something', RESULT_OF_ASYNC)
With binding ExtJS takes care of refreshing the visibility of your component every time when the value in the View Model is changed. There are good examples at the link I provided. This is a very powerful and complex feature of ExtJS, worth learning.
Related
i want to filter out the prop in computed, the prop value is available, but computed always shows undefined. Following is my code :
export default {
name: "Validation",
props: {
validationResult: {
type: Object,
required: true,
},
},
computed: {
filteredInvalidRules() {
return this.validationResult.sss.rules.filter((rule) => rule.isValid === false);
},
},
'Validation Results' is aavailable , and has value in dev Tools.
The computed property 'filteredInvalidRules' is always undefined. How to fix it?
TIA
I guess it's undefined because of the component creating steps.
I think it goes something like this (please correct me if I'm wrong):
the child component is created.
your computed property looks after validationResult.
validationResult is required, but has no value yet.
filteredInvalidRules returns undefined
your parent component is created and you pass the data (and you see them in your devTools).
Solution 1: add default values to your props
props: {
validationResult: {
type: Object,
default() {
return {
sss: {
rules: {
isValid: false
}
}
};
},
required: true,
},
},
so when your child component is created, your computed property got access to the default property values.
Solution 2: return in computed property
filteredInvalidRules() {
const isUndefined = obj2?.sss?.rules === undefined
if (isUndefined) return []
return this.validationResult.sss.rules.filter((rule) => rule.isValid === false);
},
this approach will return an empty array on creating the child component. I think it's better to use default values, to keep the computed property clean and to prevent the isUndefinedcheck each time a value changes.
There could be more solutions to it.
When you wrap a new set of blocks in the core/group block the "Inner blocks use content width" toggle switch defaults to true. There is an object attribute showing called layout for that block. I'm assuming that I can update the settings on the layout attribute like I can with the align attribute.
Here is how I'm updating the align attribute:
const { addFilter } = wp.hooks;
const { assign, merge } = lodash;
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/group') {
return assign({}, settings, {
attributes: assign( {}, settings.attributes, { align: {
type: 'string', default: 'wide'
} } ),
});
// console.log({ settings, name });
}
return settings;
}
addFilter(
'blocks.registerBlockType',
'intro-to-filters/cover-block/alignment-settings',
filterCoverBlockAlignments,
);
The above works so I assume updating the layout's default would be similar, but either I don't have the syntax for an object type correct, or possibly you can't update the layout object like you can update the align string. This is what I tried for the function:
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/group') {
return assign({}, settings, {
attributes: assign( {}, settings.attributes, { layout: {
type: 'object', [{
type: 'default'
}]
} } ),
});
// console.log({ settings, name });
}
return settings;
}
In short I'm trying to get the blocks layer attribute (which is an object and not a string) have it's attribute of type to default to "default" instead of "constrain".
I mean yes, you could solve it by filtering. What I would suggest however, is making a block-variation. Then setting that block-variation as the default. That block variation then can have any of your settings you like to set. Quite simple in theory.
By default, all variations will show up in the Inserter in addition to the regular block type item. However, setting the isDefault flag for any of the variations listed will override the regular block type in the Inserter.
source: https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/
wp.blocks.registerBlockVariation( 'core/group', {
name: 'custom-group',
// this one is important, so you don't end up with an extra block
isDefault: true,
// and here you add all your attributes
attributes: { providerNameSlug: 'custom' },
} );
Here is a slightly cleaner variant (using destructuring), if you have more code:
// extract the function from wp.blocks into a variable
const { registerBlockVariation } = wp.blocks;
// register like above
registerBlockVariation();
My rest service expose me a group of fields: each filed has a value and a list of attributes: enabled, maxLength (in case of string), minLength (in case of string), decimals (number of decimal digits - in case of float).
In OpenUi5 I have:
value and enabled are properties of Input control Link (Good!! I can bind properties with model contains the attributes)
maxLength and decimals are optionsof String type and Float type (Link) but I can't bind options with a model :-/
minLength I can't find a property/option
I would like map (bind) each attribute with component so that automatically the library control for me without writing more code.
there is a property called maxLength for Input Control.
So the only problem I see is binding minLength and decimals for which there is little bit effort is needed.
Solution
Create your own input control by extending the existing Input
Control.How to achieve it?
Sample Code Structure:
jQuery.sap.require("sap.m.Input");
jQuery.sap.declare("sap.m.ComplexInput");
sap.m.Input.extend("sap.m.ComplexInput", {
metadata: {
properties: {
minLength: {
type: "int"
},
decimals: {
type: "int"
},
events: {
//define your own events like checkMinLength,checkDecimals
}
},
onInit: function () {
//on init do something
},
onAfterRendering: function () {
//called after instance has been rendered (it's in the DOM)
},
_somePrivateMethod: function () {
/*do someting...*/
},
somePublicMethod: function () {
/*do someting...*/
},
}
});
sap.m.ComplexInput.prototype.exit = function () {
/* release resources that are not released by the SAPUI5 framework */
//do something
};
Adding CustomData and using wherever you want to.
Then you can access custom data in validation process or on liveChange or so..
Bind the other properties to the value of customData
var input = new sap.m.Input({
value: '{value}',
enabled: '{enabled}',
maxLength: '{maxLength}',
customData: [
new sap.ui.core.CustomData({
key: 'minLength',
value: '{minLength}'
}),
new sap.ui.core.CustomData({
key: 'decimals ',
value: '{decimals}'
})
],
change: function(oEvent) {
var src = oEvent.getSource();
var minLen = src.getCustomData()[0].getValue();
var decimals = src.getCustomData()[1].getValue();
if (src.getValue() && src.getValue().length > minLen) {
src.setValueState('Success');
} else {
src.setValueState('Error');
}
}
});
I'm using the following code in my view to fetch my collection from the server:
initialize: function () {
_this = this;
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i){
var todo = new TodosModel({
id: i.id,
content: i.content,
completed: i.completed
});
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
error : function(collection, response) {
console.log('ERROR GETTING COLLECTION!');
}
});
},
Which seems to work - here's the output from my server:
{
"0": {
"id": 1,
"content": "one",
"completed": false
},
"3": {
"id": 4,
"content": "two",
"completed": true
},
"4": {
"id": 5,
"content": "tester",
"completed": false
}
}
Except for the fact that if I log out my collection there is a null entry in the first position:
Which then causes issues as if I add an item it takes the ID of the last element. I'm new to backbone and am hoping I'm just missing something simple.
Here's my crack at a quick run through of your code. I haven't tested anything so there might be typos. I'm still not sure where the stray empty model is coming from but if you restructure your application as outlined below, I suspect the problem will go away.
The model and collection look okay so let us have a look at your view.
el: $('#todos'),
listBlock: $('#todos-list'),
newTodoField: $('#add input'),
//...
template: $('#todo-template').html(),
//...
events: { /* ... */ },
These should be okay but you need to ensure that all those elements are in the DOM when your view "class" is loaded. Usually you'd compile the template once:
template: _.template($('#todo-template').html()),
and then just use this.template as a function to get your HTML. I'll assume that template is a compiled template function below.
initialize: function () {
_this = this;
You have an accidental global variable here, this can cause interesting bugs. You want to say var _this = this;.
this.el = $(this.el);
Backbone already gives you a jQuery'd version of el in $el so you don't need to do this, just use this.$el.
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i) {
var todo = new TodosModel({ /* ... */ });
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
//...
The collection's fetch will add the models to the collection before the success handler is called so you don't have to create new models or add anything to the collection. Generally the render method renders the whole thing rather than rendering just one piece and you bind the view's render to the collection's "reset" event; the fetch call will trigger a "reset" event when it has fetched so the usual pattern looks like this:
initialize: function() {
// So we don't have to worry about the context. Do this before you
// use `render` or you'll have reference problems.
_.bindAll(this, 'render');
// Trigger a call to render when the collection has some stuff.
this.collection.on('reset', this.render);
// And go get the stuff we want. You can put your `error` callback in
// here if you want it, wanting it is a good idea.
this.collection.fetch();
}
Now for render:
render: function (todo) {
var templ = _.template(this.template);
this.listBlock.append(templ({
id: todo.get('id'),
content: todo.get('content'),
completed: todo.get('completed')
}));
// Mark completed
if(todo.get('completed')) {
this.listBlock.children('li[data-id="'+todo.get('id')+'"]')
.addClass('todo-completed');
}
}
Normally this would be split into two pieces:
render to render the whole collection.
Another method, say renderOne, to render a single model. This also allows you to bind renderOne to the collection's "add" event.
So something like this would be typical:
render: function() {
// Clear it out so that we can start with a clean slate. This may or
// may not be what you want depending on the structure of your HTML.
// You might want `this.listBlock.empty()` instead.
this.$el.empty();
// Punt to `renderOne` for each item. You can use the second argument
// to get the right `this` or add `renderOne` to the `_.bindAll` list
// up in `initialize`.
this.collection.each(this.renderOne, this);
},
renderOne: function(todo) {
this.listBlock.append(
this.template({
todo: todo.toJSON()
})
)
// Mark completed
if(todo.get('completed')) {
this.listBlock.find('li[data-id="' + todo.id + '"]')
.addClass('todo-completed');
}
}
Notice the use of toJSON to supply data to the template. Backbone models and collections have a toJSON method to give you a simplified version of the data so you might as well use it. The model's id is available as an attribute so you don't have to use get to get it. You could (and probably should) push the todo-completed logic into the template, just a little
<% if(completed) { %>class="completed"<% } %>
in the right place should do the trick.
addTodo: function (e) {
//...
var todo = new TodosModel({
id: todoID,
content: todoContent,
completed: todoCompleted
});
this.render(todo);
todo.save();
_this.collection.add(todo);
You could bind renderOne to the collection's "add" event to take care of rendering the new model. Then use the save callbacks to finish it off:
var _this = this;
var todo = new TodosModel({ /* ... */ });
todo.save({}, {
wait: true,
success: function(model, response) {
// Let the events deal with rendering...
_this.collection.add(model);
}
});
Again, an error callback on the save might be nice.
completeTodo: function (e) {
//...
todo.save({
completed: todoCompleted
});
}
The save call here will trigger a 'change:completed' event so you could bind to that to adjust the HTML.
removeTodo: function (e) {
//...
}
The destroy call will trigger a "destroy" event on the model and on the collection:
Any event that is triggered on a model in a collection will also
be triggered on the collection directly, for convenience. This
allows you to listen for changes to specific attributes in any model
in a collection, [...]
So you could listen for "destroy" events on the collection and use those to remove the TODO from the display. And destroying the model should remove it from the collection without your intervention.
printColl: function () {
this.collection.each(function (todo) {
console.log('ID: '+todo.get('id')+' | CONTENT: '+todo.get('content')+' | COMPLETED: '+todo.get('completed'));
});
}
You could just console.log(this.collection.toJSON()) instead,
you'd have to click around a little to open up the stuff in the
console but you wouldn't miss anything that way.
All the event binding for the collection would take place in your
view's initialize method. If you're going to remove the view then
you'd want to override the remove to unbind from the collection
to prevent memory leaks:
remove: function() {
// Call this.collection.off(...) to undo all the bindings from
// `initialize`.
//...
// Then do what the default `remove` does.
this.$el.remove()
}
You could also use a separate view for each TODO item but that might be overkill for something simple.
I have found many references on how to create a 'loading' message or mask when loading data in to a grid in Ext JS 4 via a data store / proxy (I am using direct type).
So I had added this in my controller at one point (because I was NOT getting a loading message previously) :
init: function() {
var store = this.getEncountersStore();
store.on({
beforeload: function(store,operation,eopts) {
Ext.getBody().mask('Loading...');
},
load: function(store,records,success,operation,eopts) {
Ext.getBody().unmask();
}
});
}
That seems to work for me in my MVC application, however, next I added a task manager timer to automatically refresh the grid data every 10 seconds:
this.runningTask = Ext.TaskManager.start ({
run: this.loadEncounterData,
interval: 10000,
scope: this
});
loadEncounterData: function() {
var store = this.getEncountersStore();
store.load({
params: {
},
callback: function(r,options,success) {
if(success == true)
...
} //callback
}); //store.load
I noticed that there were now TWO 'loading' mask messages on the screen!
So, I removed my 'store.on' code block above from my controller init, and now I have only one message.
So where does the other message come from?
Is it part of a Grid?:
Ext.define('ESDB.view.encounter.List', {
extend: 'Ext.grid.Panel',
...
I found a page that seems to asking the same question, though I was not able to figure out how to get it to work, or how to do it according to ExtJS 4 / MVC.
loadMask is not a config in Grid panel.
You can add as a config in gridpanel
viewConfig : {
loadMask: false
}
The loadMask is part of the gridView.
http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.View-cfg-loadMask
GridPanel components all have a gridView component that defines various things to do with the table view in the panel.
To prevent a loadMask on a grid, you set config for loadMask to false, IE:
Ext.define('ESDB.view.encounter.List', {
extend: 'Ext.grid.Panel',
loadMask : false,
...
You could change your load function to just load the store:
loadEncounterData: function() {
var store = this.getEncountersStore();
store.load();
...
Then you could use the following approach to automatically handle the loadMask whenever the grid store loads.
Using Ext.util.DelayedTask is handy to prevent the loadMask from appearing if the load takes less than 500ms.
Ext.define('ESDB.view.encounter.List', {
extend: 'Ext.grid.Panel',
...
initComponent: function() {
var me = this;
me._mask = new Ext.LoadMask(me, {msg: 'Loading...'});
me._maskd = new Ext.util.DelayedTask(function() {
me._mask.show();
});
me.store = Ext.create('Ext.data.Store', {
...
listeners: {
beforeload: function() {
me._maskd.delay(500);
...
},
load: function() {
me._maskd.cancel();
me._mask.hide();
...
}
}
});
...