Would love to read your thoughts on how you would model a input (textfield) with xState.
According to an input ux article a textfield could have following states:
Input text fields can have one of the following states: default, focused, error, and disabled. All states should be clearly differentiated from one another.
This makes sense as other libs like Material UI uses more or less the same states.
Im wondering how to model this.
Let me just write down some thoughts:
I think its obvious that the value should be part of the xState context as it could have any value
the mentioned states makes sense as well
Now the part where im not so sure: lets say we have an inline validation (onChange) which says the textfields value is ok and for that we set want to set with css a class "valid" which gives the textfield a green border.
We would need either a state transition from default to default_valid (not just valid because we are still in the default state) ... the same applies for default_invalid ... and aswell for some more combinations which would possible end in a state explosion.
model it in xState as a child state and access it via default.valid or default.invalid ...
In both scenarios we would need in the textfield component another mapping which reads something like
(just pseudocode)
switch(state) {
'default.invalid': setColor(red), setDisabled(false)
'default.valid': setColor(green), setDisabled(false)
'default.valid.submitting': {
setColor(green)
setDisabled(true)
}
Im really not happy with this approach to manage the state in the component and time in xState machine. This just seems wrong to me.
I would prefer to just use the input machines states ... which works well for some default like, default, focused ... but as soon as the field is "in 2 or more states" it gets a mess.
One way would be to just keep some high level states and write the additional ones in the context and just pass it to the textfield? (sounds not that good either tbh)
So would love to hear your thoughts how you would model something like this.
You can use state.matches(...) to clean things up, and/or you can place those actions directly in the states they're relevant for, e.g., in entry or exit.
Related
I'm trying to prevent content replacing for angular app in my custom component, it happening when on custom component like <my-component></my-component> placed BOUNDED property textContent/innerHTML/innerText, e.g. <my-component [textContent]="example"></my-component> and my component just dying without message.
as i know for now angular aplying content replacing with bounded defualt propertys in the moment afterViewInit in the same moment where it apply interpolation it means i can't split them by checking something like elementRef.nativeElement.textContent/innerHTML/innerText in the afterViewInit(or any else).
also i can't get it by getAtribute/hasAtribute you will got null/false if you will try to do this on bounded textContent/innerHTML/innerText (maybe it's bug idk can some one answer for that pls)
so for now i have two main idea how can i do this
i can check some unique element with BEM notation still exist inside my component in afterViewInit but it's to local.
i can catch bounded property by input field with the same name as property but it's to dirty even if i need check only one but i wanna check them all(3).
(my senior colleague prompted me) i can check elementRef.nativeElement.children !== 0 that looks good but we will be ignore innerHTML with elements inside
maybe you have better idea how i can to do this
i use angular 12 with ivy mod and i was't test it on other version
EDIT: CodePen link here.
I'm (ab)using JSX to make a form-building 'DSL' for some of my non-technical colleagues. There's a SingleChoice component that can be used like this:
<SingleChoice>
<Option value="A">
// ... what to show when A is chosen
</Option>
<Option value="B">
// ... what to show when B is chosen
</Option>
</SingleChoice>
The result is a <div> full of radio button inputs, and under the block of inputs, there are the conditional elements (based on what option is chosen).
In other words, the Option elements don't render anything by themselves, they are there just to signal to the parent how many radio buttons there are, what are their labels and what should be shown when X is chosen. They are literally empty shells, made just to carry their props, like this:
function Option({label, value, children}) { return <></> }
Yes, I could instead pass those as an array of objects { value: string, show: ReactNode }, but that's not a friendly syntax for my non-dev colleagues.
Now, to the question. In the parent I go through all his children and render the input based on their value:
...
{children.map(ch => <> <input ... /> {ch.props.value} </>)}
...
The problem is, this only works when I manually pass a value prop to the Option component. E.g. when I have
function Yes({..., value = "Yes"}) { ... }
and I do
<SingleChoice>
<Yes />
</SingleChoice>
the label is empty, as if it didn't see the default prop value. Why is that happening? Is it a bug? And how do I implement this properly? Remember, I don't really want to expose any of the implementation details to the user who writes the form (so no explicit callback passing).
The only "proper" way I could think of would be creating a context with a callback in the parent, which all the children would look up and call with their values. The problem is, there would be a lot of contexts made and updated this way, and I fear the performance implications.
I'm a bit confused as to what you want to have happen. As far as I can tell from your code on Codepen, a default prop value is never defined so when you call
<Yes/>
without passing it a prop it doesn't get rendered by your parent component. Also, a react function should just accept props as a parameter, you don't need to define value and children.
I'm answering in another answer to get the formatting
The first error you have is you run ReactDOM.render in the beginning of your code. You should add it at the end of the code, or else default values won't be used(I don't know why this is).
The second is that your functions aren't really react. In react functions take one parameter, props. You can reference value or children as props.value or props.children.
not react: function Yes({value = "Yes1", children}) { return <span></span> }
react: function Yes(props){ return <span>?+{props.value}</span> }
The third is that declaring defaultProps is done through the defaultProps command in react. So instead of setting a parameter to a value(because again, no parameters) you should write:
Yes.defaultProps={ value:"Yes1" }
Finally, your function singleChoice isn't really react as well. Theres definitely a cleaner way to refactor your code then scrolling through the yes components and checking their value. However, you can do what you want. If you fix the other problems it should work as you intended.
My List Binding:
<StandardListItem
title="{NewProposalTitle_DE}"
description="Stream: {Stream}, Time: {starttime}"
press="handlePress"
>
I want to change the title's binding to title="{NewProposalTitle_EN}" from the oData model, when the user is english.
I looked into expression binding but this doesn't seem to work with dynamic data:
title="{= {language} === "de" ? {NewProposalTitle_DE} : {NewProposalTitle_EN} }
So I tried a different approach where I iterate through all items from a SAPUI5 List and want to change the (german) title attribute to the english one if the language of the user is english and add a number to the Item's counter attribute.
var Items = this.getView().byId("idList").getItems();
for (var i = 0; i < Items.length; ++i) {
if(language === "en"){
Items[i].setTitle(oData.results[i].Title_EN);
}
Items[i].setCounter(numbers[i]);
}
If the language is german, everything works like a charm and the counter is set correctly, but if the language is english, things get fishy.
When the Title gets updated with the english title, the counter is set back to 0 (default). Even more strange, if the german and the english version of the title are equal (so no change needs to be done) the counter is still shown (i assume SAPUI5 is smart enough to recognize, that no change needs to be done).
I encountered the same problem, when I tried to add a Style Class for the Item:
Items[i].addStyleClass("blue");
again, as long as the title doesn't get changed it works.
I tried changing the order in every possible way without success.
I also tried to setCounter and addStyleClass and omitted the setTitle command but only one affected the DOM.
Please don't translate this way! You might end up overwriting data in your other models. Use the i18n resource models to do your translations! It's the go-to, built in way to translate any text in your application to any other language.
SAPUI5 loads the right ones by default.
If your browser's language is set to German, it will try to load i18n_de_DE.properties first, (not sure of the exact name for German, you'll find out if you look at the network tab of the browser. Could be just _de.properties if there's such a thing as _de_AT or something) before falling back to the default i18n.properties.
If you start an application from any of the usual IDE's like webIDE or eclipse, the model is already created for you.
The idea is to set the title to an identifier, like
<Label text={i18n>MY_TRANSLATABLE_TEXT} />
In your i18n.properties file, you might default to English:
MY_TRANSLATABLE_TEXT=Customer
While your German properties file has
MY_TRANSLATABLE_TEXT=Kunde
The translation is then done for you without trying to mess with the titles directly.
Thank you all for your answers! I figured out a solution for my problem:
In the controller, I check for the users language and update the property binding:
Items[i].bindProperty("title", "NewProposalTitle_EN");
It may not be the most elegant or correct way, but it works for me (at the moment).
I hope you all have a great day!
title="{= {language} === "de" ? {NewProposalTitle_DE} : {NewProposalTitle_EN} }
There are some syntactical issues in your expression binding. Try with this:
title="{= ${language} === 'de' ? ${NewProposalTitle_DE} : ${NewProposalTitle_EN} }"
Besides that, I don't think it's a good idea to put translatable texts into the entity set as properties. I'd recommend to define field label (e.g. for the NewProposal property) translated in each language as a UI text annotation
(such as sap:label).
When an ODataModel is created, the service metadata will be requested immediately with the corresponding language query sap-language =<automatically detected>.
Thus the property labels come already translated from the backend system, contained in the metadata, which you can bind in the view via special property binding syntax. E.g.:
In Metadata
<EntityType Name="BusinessPartner" sap:content-version="1">
<Key>
<PropertyRef Name="BusinessPartnerID" />
</Key>
<Property Name="BusinessPartnerID" Type="Edm.String"
sap:label="Business Partner ID"
sap:creatable="false"
sap:updatable="false"
/>
...
</EntityType>
Property Metadata Binding
<StandardListItem
title="{myModelName>BusinessPartnerID/##sap:label}"
press=".handlePress"
/>
I am trying to process an insert event from the CKEditor 5.
editor.document.on("change", (eventInfo, type, data) => {
switch (type) {
case "insert":
console.log(type, data);
break;
}
});
When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:
{
range: {
start: {
root: { ... },
path: [0, 14]
},
end: {
root: { ... },
path: [0, 15]
}
}
}
I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?
Is there something I am missing, or is there just a different way to do this all together?
First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.
EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.
Until March 2018 (up to 1.0.0-alpha.2)
(If you need to learn more about some class API or an event, please check out the docs.)
Your best bet would be simply to iterate through the inserted range.
let data = '';
for ( const child of data.range.getItems() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.
(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)
Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).
Doing changes on Document#change
You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:
more changes can come at once from collaboration service,
a different feature might have already reacted to the same change and enqueued its changes which might make the model different.
If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).
If you would like to have this solution bulletproof, you probably would have to do this:
While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
Do your logic for each found TextProxy instance.
Remember to destroy() all the LiveRanges afterwards.
As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.
Since March 2018 (starting from 1.0.0-beta.1)
The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.
First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.
Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).
Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.
Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:
editor.document.registerPostFixer( writer => {
const changes = editor.document.differ.getChanges();
for ( const entry of changes ) {
if ( entry.type == 'insert' && entry.name == '$text' ) {
// Use `writer` to do your logic here.
// `entry` also contains `length` and `position` properties.
}
}
} );
In terms of code, it might be a bit more of it than in the first snippet, but:
The first snippet was incomplete.
There are a lot fewer edge cases to think about in the new approach.
The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.
The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.
You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)
#Szymon Cofalik's answer went into a direction "How to apply some changes based on a change listener". This made it far more complex than what's needed to get the text from the Document#change event, which boils down to the following snippet:
let data = '';
for ( const child of data.range.getChildren() ) {
if ( child.is( 'textProxy' ) ) {
data += child.data;
}
}
However, reacting to a change is a tricky task and, therefore, make sure to read Szymon's insightful answer if you plan to do so.
I am trying to use ngGrid to make somewhat of a "tree-control" which I can build dynamically by calling API's. ngGrid allows for grouping on rows, yet the nature of it requires that all rows be present at the beginning. This is unfortunate for the fact that an API to pull back all generation data for a File Integrity Monitoring system would be insanely slow and stupid. Instead, I wish to build the "tree" dynamically on the expansion of each generation.
I am trying to inject children (ngRows) into a group-row (ngAggregate) on a callback, yet I do not think that I am calling the correct constructor for the ngRows for the fact that the rows are ignored by the control
Through the use of the aggregateTemplate option on the gridOptions for ngGrid, I have been able to intersept the expansion of a group quite easily.
(maybe not easily, but still)
I've replaced the ng-click of the default template:
ng-click="row.toggleExpand()"
with:
ng-click="$parent.$parent.rowExpanded(row)"
I know that it's a bit of a hack, but we can get to that later. For now, it gets the job done.
The way that I discovered how to work my way up the $scope to my rowExpanded function was by setting a breakpoint in ngGrid's "row.toggleExpand" function and calling it from the template as so:
ng-click="row.toggleExpand(this)"
Once I retrieve the group I want, I call an API to get the children for said group. I then need to make the return as children of the row. I decided to do this by calling ngGrid's ngRow factory:
row.children = [];
for(var i = 0; i < childData.length; i++)
{
row.children[row.children.length] = row.rowFactory.buildEntityRow(childData[i], i);
}
row.toggleExpand();
... yet this does not appear to be working. The rows are not showing up after I do the expand! Why won't my rows show up?
Here's my current Plunker!
By the way
I've placed a debugger statement within the group-expand callback. As long as you have your debugger open, you should catch a breakpoint on the expansion of a group.
Thanks everybody!
I found my answer, I'm an idiot....
I got this control working, and then realized that it was a total hack, that I could have used the control the way it was meant to be used and it would have worked much better, had much better work-flow, and it would have saved me an entire day of development. If you are wondering how you use the control this way, the answer is that you don't.
I got the stupid thing to work by updating my data structure after the round trip and forcing the grid to refresh, pretty obvious. I had to set the grid options so that groups were always expanded and I had to control the collapser icon logic myself, outside of ngGrid. I never called row.toggleExpand. I also hid any rows with null values by a function call within an ng-if on my rowTemplate. After all that was said and done, I put my foot in my mouth.