I am using GetX for the state management in flutter project. My problem is when I click on the checkbox it updates the state in controller but does not show the result in UI. In general the changed state should change the UI too. I am not sure what is the problem but I think there is a bug in using the observable variable in getx. How to solve this problem? If the post is not clear then please ask me.
This is the view.
class FilterBottomSheetWidget extends GetView<SearchController> {
#override
Widget build(BuildContext context) {
return Container(
height: Get.height - 90,
child: Stack(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 80),
child: Container(
padding: EdgeInsets.only(top: 20, bottom: 15, left: 4, right: 4),
child: controller.expiringContractStatus.isEmpty ? CircularLoadingWidget(height: 100)
: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: ExpansionTile(
title: Text("Contract Status".tr, style: Get.textTheme.bodyText2),
children: List.generate(controller.expiringContractStatus.length, (index) {
var _expiringContractStatus = controller.expiringContractStatus.elementAt(index);
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
value: _expiringContractStatus["isCheck"], // ************
onChanged: (value) {
controller.itemChange(value, index); // ************
controller.update();
},
title: Text(
_expiringContractStatus["title"],
style: Get.textTheme.bodyText1,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
);
}),
initiallyExpanded: true,
);
}
)
),
),
// more widgets
],
),
);
}
}
This is SearchController.
class SearchController extends GetxController {
final expiringContractStatus = <Map>[
{
"title": "aaa",
"isCheck": false
},
{
"title": "bbb",
"isCheck": false
},
{
"title": "ccc",
"isCheck": false
}
].obs;
#override
void onInit() async {
await refreshSearch();
super.onInit();
}
void itemChange (bool value, int index) {
expiringContractStatus[index]["isCheck"] = value; // *************
update();
}
}
Updated Answer:
If you tried wrapping an an Obx and it didn't work, use GetBuilder<SearchController> instead.
GetBuilder<SearchController>(
builder: (_) => controller.expiringContractStatus.isEmpty ? CircularLoadingWidget(height: 100)
: SingleChildScrollView(...))
When you call update() it will trigger a rebuild no matter what, with the updated values. The only reason I can think of why Obx didn't work is that perhaps when it comes to lists in only responds to an addition or removal of an item in the list and not a change in a property nested inside.
In general, you'll probably find that in most cases using an observable stream based variable is not really needed. GetBuilder can handle most or all of what you need done unless you're doing something stream based (binding to an external Firebase collection for example). Nothing wrong with either, but GetBuilder is the most performant option as streams by their nature are a bit more expensive.
Original answer:
I suggest you read the docs regarding proper usage before claiming there's a bug in someone else's code. That goes for any package that you're using.
It's stated all over the Readme and in the basic counter app example that observable variables need to be placed in an Obx widget, otherwise there's nothing triggering a rebuild based on the updated state of an observable GetX variable.
The child of your Container should be
Obx(() => controller.expiringContractStatus.isEmpty ? CircularLoadingWidget(height: 100)
: SingleChildScrollView(...))
Related
I am using Algolia to build up a refinement list of filters, and I have customized the algolia instance to display facets with no matches: Algolia docs on displaying facets with no matches
The issue I am having is that in the mounted function in VueJs Algolia is building up the refinement list, and then doing some magic to build the Dom. I am having an issue where I am trying to append a CSS class to these elements in the same mounted function but the NodeList I am working on is constantly returning [] empty.
My solution is completely working when typing, and when using the refinement list, but its not working on page reload as the timing of things is out of sync.
mounted() {
this.searchClient
.searchForFacetValues([
{
indexName: this.indexName,
params: {
facetName: this.brandAttribute,
facetQuery: '',
maxFacetHits: this.brandLimit,
},
},
])
.then(([{ facetHits }]) => {
this.initialFacets.push(
...facetHits.map(facet => ({
...facet,
label: facet.value,
value: facet.value,
isRefined: false,
count: 0,
}))
);
});
console.log(this.$el.querySelectorAll(".ais-RefinementList-list .ais-RefinementList-count")); //Returns empty
setTimeout(() => {
console.log(this.$el.querySelectorAll(".ais-RefinementList-list .ais-RefinementList-count")); //Is populated
}, 5000);
},
I have set a timeout to prove this is a timing issue. Inside my timeout The nodelist is returning as expected, and I can add my CSS class, but how long do I wait? I don't believe this is a correct solution
I have tried all VueJs lifecycle hooks and in all the NodeList is returning null.
What I need is:
After the page has completly loaded, loop over my nodeList and Add CSS. The way I am doing this is:
document.querySelectorAll(".ais-RefinementList-list .ais-RefinementList-count").forEach(
function(x) {
if(x.innerHTML == 0) {
x.parentElement.style.backgroundColor = "red"
} else {
x.parentElement.style.backgroundColor = "white"
}
}
);
Working Solution:
Page Refresh, timing issue:
Putting code in timeout:
setTimeout(() => {
this.$el.querySelectorAll(".ais-RefinementList-list .ais-RefinementList-count").forEach(
function(x) {
if(x.innerHTML == 0) {
x.parentElement.style.backgroundColor = "red"
} else {
x.parentElement.style.backgroundColor = "white"
}
}
);
}, 5000);
Gives this solution after 5 seconds: Working:
I was able to overcome this with the solution of customizing the UI: Algolia docs, customizing the UI
I completly overwrote my Refinement list, and conditionally added the needed classes using the :style binding directive.
<ais-refinement-list
attribute="ParentPageTitle"
:transformItems="transformItems"
>
<label
slot="item"
slot-scope="{ item }"
class="ais-RefinementList-label"
:style="[item.count ? {'background-color' : 'white'} : {'background-color' : 'red'}]"
>
<input type="checkbox" class="ais-RefinementList-checkbox">
<span class="ais-RefinementList-labelText">{{item.value}}</span>
</label>
</ais-refinement-list>
CSS is now able to correctly style the component on runtime, and the timing issue has been resolved
we use the rich text editor of TipTap in our project.
But we have the problem, that spaces are not recognized correctly and only after every 2 click a space is created. As framework we use Vue.JS.
import { Editor, EditorContent, EditorMenuBar } from 'tiptap'
import {
HardBreak,
Heading,
OrderedList,
BulletList,
ListItem,
Bold,
Italic,
History
} from 'tiptap-extensions'
import EditorMenuButton from './EditorMenuButton.vue'
export default {
name: 'editor',
components: {
EditorMenuButton,
EditorMenuBar,
EditorContent
},
props: {
value: {
type: null,
default: ' '
}
},
data () {
return {
innerValue: ' ',
editor: new Editor({
extensions: [
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new BulletList(),
new OrderedList(),
new ListItem(),
new Bold(),
new Italic(),
new History()
],
content: `${this.innerValue}`,
onUpdate: ({ getHTML }) => {
this.innerValue = getHTML()
}
})
}
},
watch: {
// Handles internal model changes.
innerValue (newVal) {
this.$emit('input', newVal)
},
// Handles external model changes.
value (newVal) {
this.innerValue = newVal
this.editor.setContent(this.innerValue)
}
},
mounted () {
if (this.value) {
this.innerValue = this.value
this.editor.setContent(this.innerValue)
}
},
beforeDestroy () {
this.editor.destroy()
}
}
</script>
does anyone have any idea what could be the reason for assuming only every two spaces?
We had the same problem, we kept the onUpdate trigger but changed the watch so that it would only invoke editor.setContent when the value was actually different.
watch: {
value() {
let html = this.editor.getHTML();
if (html !== this.value) {
this.editor.setContent(this.value);
}
},
},
"Okay the problem is that the watcher will get fired when you type in the editor. So this will check if the editor has focus an will only update the editor content if that's not the case."
watch: {
value(val) {
if (!this.editor.focused) {
this.editor.setContent(val, false);
}
}
},
issue: https://github.com/ueberdosis/tiptap/issues/776#issuecomment-667077233
This bug for me was caused by doing something like this:
watch: {
value: {
immediate: true,
handler(newValue) {
this.editor.setContent(newValue)
},
},
},
Removed this entirely and the bug went away. Maybe this will help someone in future.
Remove onUpdate section and the bug will disapear. I don't know why, but it's interesting to know how to reproduce the bug.
That does help. Following this advice, I am currently using the onBlur event instead of onUpdate, while obtaining the content's HTML using the editor instance and the getHTML() function, as such: this.editor.getHTML().
(In my case I $emit this value in order for it to be reactive to my parent component, but that may be irrelevant for the original question).
Maybe you should try this.
watch: {
// Handles external model changes.
value (newVal) {
// convert whitespace into \u00a0 ->
let content = newVal.replace(/\s/g, "\u00a0");
this.editor.setContent(content)
}
},
It seems like the normal white space has been removed by html automatically. Therefore, I convert whitespace into 'nbsp;' and it's worked.
The code you provided seems to be working just fine. So the issue most likely is produced by a side effect in either your code or some dependency.
To debug this issue you could look for event listeners, especially regarding key press or key down events and looking if you are checking for space key specifically somewhere (event.keyCode === 32 or event.key === " "). In conjunction with event.preventDefault this could explain such an issue.
Another more broad way to debug this is to strip away parts from your code until the bug disappears or add to a minimal example until the bug appears.
Remove onUpdate section and the bug will disapear. I don't know why, but it's interessing to know how to reproduce the bug.
However if you create a "minimal reproductible example" the bug does not appear.
So what ? I don't know.
I found a workaround which is to use vuex.
Rather than assign the value returned by getHTML() in the innerValue variable and then issue an 'input' event, I put this value in the store.
I have one big request for you. I am a high-school student and I want to create an app for students with my friend. In the begining we wanted to use React for our reactive components, but then we saw Vue and it looked really good. But because of the fact, that we already have a big part of the app written in twig, we didn't want to use Vue.js standalone, because we would have to change a lot of our code, especially my friend, which is writing backend in Sympfony. So we use the runtime only version, which does not have a template option, so i have to write render functions for our components. And i am stucked with one particular problem.
I am writing a file-manager, and i need to render layer for every folder. Code is better then million words, so, take a look please :
var data = {
name: 'My Tree',
children: [
{
name: 'hello',
isFolder: false,
},
{
name: 'works',
isFolder: true,
children: [
{
name: 'child2',
isFolder: true,
},
{
name: 'child3',
isFolder: false,
},
]
}
]
}
Vue.component('layer', {
render: function renderChild (createElement) {
if(data.children.length){
return createElement('ul', data.children.map(function(child){
return createElement('li', {
'class' : {
isFolder: child.isFolder,
isFile: !child.isFolder
},
attrs: {
id: "baa"
},
domProps: {
innerHTML: child.name,
},
on:{
click: function(){
console.log("yes");
},
dblclick: function(){
console.log("doubleclicked");
if(child.children.length){
// if this has children array, create whole "layer" component again.
}
}
}}
)
}))
}
},
props: {
level: {
type: Number,
required: true
},
name: {
type: String,
}
}
})
new Vue({
el: '#fileManagerContainer',
data: data,
render (h) {
return (
<layer level={1} name={"pseudo"}>
</layer>
)
}
})
My question is, how to write that recursive call, which will render the whole Layer component on the doubleclick event, if the element has children array.
Thank you in advance for any reactions, suggestions or answers :)
I know this is a very old question, so my answer won't be useful to the OP, but I wanted to drop in to answer because I found myself with the very same problem yesterday.
The answer to writing these recursive render functions is to not try to recurse the render function itself at all.
For my example, I had a set of structured text (ish) - An array of objects which represent content - which can be nested, like so:
[
// each array item (object) maps to an html tag
{
tag: 'h3',
classes: 'w-full md:w-4/5 lg:w-full xl:w-3/4 mx-auto font-black text-2xl lg:text-3xl',
content: 'This is a paragraph of text'
},
{
tag: 'img',
classes: 'w-2/3 md:w-1/2 xl:w-2/5 block mx-auto mt-8',
attrs: {
src: `${process.env.GLOBAL_CSS_URI}imgsrc.svg`,
alt: 'image'
}
},
{
tag: 'p',
classes: 'mt-8 text-xl w-4/5 mx-auto',
content: [
{
tag: 'strong',
content: 'This is a nested <strong> tag'
},
{
content: ' '
},
{
tag: 'a',
classes: 'underline ml-2',
content: 'This is a link within a <p> tag',
attrs: {
href: '#'
}
},
{
content: '.'
}
]
}
]
Note the nested elements - These would need recursion to render properly.
The solution was to move the actual rendering work out to a method as follows:
export default {
name: 'block',
props: {
block: {
type: Object,
required: true
}
},
methods: {
renderBlock (h, block) {
// handle plain text without a tag
if (!block.tag) return this._v(block.content || '')
if (Array.isArray(block.content)) {
return h(block.tag, { class: block.classes }, block.content.map(childBlock => this.renderBlock(h, childBlock)))
}
// return an html tag with classes attached and content inside
return h(block.tag, { class: block.classes, attrs: block.attrs, on: block.on }, block.content)
}
},
render: function(h) {
return this.renderBlock(h, this.block)
}
}
So the render function calls the renderBlock method, and that renderBlock method is what calls itself over and over if it needs to - The recursion happens within the method call. You'll see that the method has a check to see whether the content property is of an Array type - At this point it performs the same render task, but rather than passing the content as-is, it passes it as an Array map, calling the same render method for each item in the array.
This means that, no matter how deeply the content is nested, it will keep calling itself until it has reached all the way to the "bottom" of the stack.
I hope this helps save someone some time in the future - I certainly wish I'd had an example like this yesterday - I would have saved myself a solid couple of hours!
I'm wondering about the most efficient, safe and smart way to code a complex form in javascript.
Often a user form can be very complex with a lot of different states, complex checks and so on, and to do a good job a good concept is absolutely necessary.
I really believe that the best solution is a state machine.
The following code is the form core logic I did for user registration with an RFID tag key, where user can register themselves using different credentials as phone, key RFID tag, mail, and complete their registration at a later stage with the same forum.
Consider just the logic behind it at high level.
The main loop which iterates on possible transitions (in order of priority):
/* Evaluate the actual form state (status) and check if it's possible to change
* to another status with a greater priority.
* If the state transition conditions are verified the transition callback is executed
* which, in case the state doesn't, is an empty function.
*/
setNextFormStatus: function(field) {
var sts,
that = this;
function conditionsAreValid(sts) {
var context = that.statusToConditionsMap[sts];
return ( sts == 'new_account' || that[context.field].state == context.state );
}
for (sts in this.statusToConditionsMap[this.status.actual].changes) {
var transition = this.statusToConditionsMap[this.status.actual].changes[sts];
if (transition && conditionsAreValid(sts)) {
if (sts != this.status.actual) {
this.status.previous = this.status.actual;
this.status.actual = sts;
this._resetForm(); // simple reset function
transition.apply(this);
}
break;
}
}
}
All status, their transition conditions, and their transition callbacks are defined in a dictionary when status are listed in order of priority:
/*
* This is the dictionary which defines form status states machine
*
* For each status are defined the following attributes:
* · state & field: define the condition to enter this status. The field must have that state (i.e. field.state == state)
* · changes (optional): the list of possible next status from this one, ordered by priority. Each status has an handle to call
*/
this.statusToConditionsMap = {
'registered_key':{
state: 'registered',
field: 'key'
},
'processed_key_with_username':{
state: 'unlinked',
field: 'key',
changes: {
'processed_key_with_username': function() {}, // empty cause callback is unnecessary
'new_account': this._checkNotEmptyFields
}
},
'phone_already_present_confirmed':{
state: 'already_present_confirmed',
field: 'phone',
changes: {
'phone_already_present_confirmed': function() {},
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'new_account': this._checkNotEmptyFields
}
},
'phone_already_present_unconfirmed':{
state: 'already_present_unconfirmed',
field: 'phone',
changes: {
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': function() {},
'new_account': this._checkNotEmptyFields
}
},
'email_already_present':{
state: 'email_already_present',
field: 'email',
changes: {
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'email_already_present': function() {},
'new_account': this._checkNotEmptyFields
}
},
'new_account':{
state:'',
field: '',
changes: {
'registered_key': this.registered_key_data_filler,
'processed_key_with_username': this.processed_desikey_data_filler,
'phone_already_present_confirmed': this.phone_already_present_data_filler,
'phone_already_present_unconfirmed': this.phone_already_present_unconf_data_filler,
'email_already_present': function() {this.showMailCheckbox(); this.runCheck('phone');},
'new_account': function() {}
}
}
}
Which can be a best practice to implement a complex form?
Any other solution or method will be appreciated.
I'm trying to build an edit column, but my routine isn't quite right for some reason. My value of "store" is not returning anything like I thought it would.
Any thoughts?
function editLinkRenderer(value, metadata, record, rowIndex, colIndex, store) {
if (store == V2020.ServiceStore)
return 'Edit';
else if (store == V2020.PriceStore)
return 'Edit';
else if (store == V2020.PromoStore)
return 'Edit';
return "Edit";
}
I'm using it in my gridpanel like so:
{ header: "Edit", width: 60, dataIndex: 'serviceID', sortable: false, renderer: editLinkRenderer },
You might consider using an ActionColumn. That way you can do this:
var items = [ ... ]; // existing items
if (store.constructEditColumn) {
items.push(store.constructEditColumn());
}
Where your constructEditColumn might look like this:
...
constructEditColumn: function() {
return {
xtype: 'actioncolumn',
items: {
text: 'Edit',
handler: function() {
// do stuff
},
scope: this
}
}
},
...
Barring that, I'd be suspicious of doing equality on the stores. Are the two params before store ints? Can you breakpoint and take a look at whether the record.store property is what you expect? Old version of Ext, perhaps, with a different signature to the renderer?
I appreciate you taking a look, but I figured out the issue.
I had two V2020.ServiceStore defined by mistake and the latter one was mucking everything up.