Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlled component behavior in React wrapper #489

Open
agusterodin opened this issue Apr 27, 2020 · 52 comments
Open

Controlled component behavior in React wrapper #489

agusterodin opened this issue Apr 27, 2020 · 52 comments

Comments

@agusterodin
Copy link

agusterodin commented Apr 27, 2020

Instead of relying on callbacks to gather information about what happened as far as tag additions/changes/deletion, the tag input should be driven off of React state (ie: an array of tags as a single source of truth). Right now the source of truth is self contained within the tagify instance itself. Right now it behaves as an uncontrolled component whereas a controlled component would be ideal!

To make this possible, you would need to add a function that lets you manually set the contents of the tag input array (pass an array and it will change the contents of the tag input to reflect this new array).

It would also involve adding an option to instance factory that tells the library not to automatically add/remove/change the tag in its own internal tag state, but still provide information on what were to happen in handler functions so that we are given the necessary information to know how to alter the react state (which in turn will cause the controlled tagify component to be updated based on the array from state we feed into it).

@agusterodin
Copy link
Author

This "controlled" behavior could also greatly enhance the experience with other view library/framework wrappers

@agusterodin agusterodin changed the title Proper React wrapper Proper controlled React wrapper Apr 27, 2020
@agusterodin
Copy link
Author

The only super confusing part would be how to handle animations during state changes, especially since tagify would no longer have any idea of identity (especially if you have duplicates enabled and have duplicate tags). The animations make everything a lot harder.

I have seen other libraries that let you manually set the complete contents like the "setTokens" method this: http://sliptree.github.io/bootstrap-tokenfield/#methods which I saw here #45. It doesn't look like they have animations though.

@agusterodin
Copy link
Author

Also, rendering things onto the DOM between state changes would require either: a manual deletion and recreation of all tag DOM elements on every state change, intelligently determining how/if to mutate the DOM by yourself, or intelligently determining how/if to mutate the DOM with something like React.

At that point having this library operate in a "controlled" manner would likely cause you to need the assistance of a view library which kinda makes me think that the only elegant way to achieve this would be to build the library from the ground up in React.

These are just my thoughts. I have written react wrappers myself but this scenario is super awkward. I would be willing to help out if needed.

@agusterodin
Copy link
Author

agusterodin commented Apr 27, 2020

You could have it so that state is formed as such:

<Tagify
tags={[{guid: 234asdfas, value: "testvalue"}, {guid: abc23452, value: "anothertestvalue"}]}
/>

That way identity can be maintained for animation purposes, especially when duplicates are enabled. This guid information would be provided along with the onAdd, onChange, onDelete events.

Also obviously the onAdd, onChange, and onDelete handlers would fire only when something was modified directly inside the tag input/list and not for additions/modifications/deletions that were made as a result of react passed in state changes.

@agusterodin
Copy link
Author

Like where it can operate like this for example

const [tags, setTags] = useState(["Kevin", "Box"])


return(
<div>
<div onClick={() => setTags([])>Clear All</div>
<Tags
      value={tags}
      onChange={(event) => {
          setTags(event.target.value) 
      }}
    />
</div>
)

@yairEO
Copy link
Owner

yairEO commented Apr 30, 2020

Currently when you update the value prop of the <Tags> React component, it internally calls loadOriginalValues() method, which clean everything up and re-create all tags.

it's not intelligent at all, but in terms of performance totally negligible because I cannot imagine thousands of tags exists and re-created.

I think that below a couple hundred tags it's totally fine.

I'm trying to keep the core code from being bloated, and am very hesitant on adding new features/requests which requires me to write a lot of new code.

If you want to disable the tags removal transition, you can set the CSS variable --tag-hide-transition to 0s

@yairEO yairEO closed this as completed May 1, 2020
@agusterodin
Copy link
Author

I agree that the "load original values" thing isn't an issue and makes sense.

The only issue is that feeding state back into the component literally doesn't work. No react library or wrapper I have ever used in my years of react development has the sort of deficiency I describe below.

Consider this example:

const [tags, setTags] = useState(["Kevin", "Box"])

return(
<div>
<div onClick={() => setTags([])>Clear All</div>
<Tags
      value={tags}
      onChange={(event) => {
          setTags(["Some value!!!"]) 
      }}
    />
</div>
)

The desirable and proper react behavior for this would be that regardless of what tag you attempt to create/delete/change, the form would not make the change but instead immediately set the tag input to show a tag that says "Some value!!!".

@yairEO
Copy link
Owner

yairEO commented May 13, 2020

ok ok no need to get angry, I understand you want a "controlled" component, let me think how can this be done.

But can you at least explain to me why do you want it to behave like that?
in what way does it not work for you well currently?

@agusterodin
Copy link
Author

My bad!! I didn't mean to come off as rude.

Having the onChanged event set the react state which feeds back into 'value' prop causes a double firing of events (namely when you press backspace to delete a tag). You also get a firing of the onchange event when initializing if you provide a 'value' prop.

I also get a react warning saying "can not flush updates while react is already rendering" every time a tag changes.

In general, the reason controlled behavior is so important is we have a very complex react + redux application and the tag input needs to be properly synced with single source of truth. Updating the tags needs to be done bidirectionally (sometimes through input itself, some times changed by redux). It makes it really hard to do this if library is not well suited towards "controlled" behavior.

On a side note: It would also be a very nice convenience if the callback would provide the tag list as an array as opposed to having the user use event.target.value to get a string of the array which then needs to be converted into an array using something like JSON.stringify. Giving it directly as an array would lend itself very well to being a controlled component.

As for approach, I'm a fan of how this library implements controlled behavior: https://github.com/prakhar1989/react-tags

@yairEO
Copy link
Owner

yairEO commented May 15, 2020

Tagify was built in javascript and not in React and in vanilla-world, there are no controlled-components, so the wrapper doesn't magically transforms Tagify to be controlled, it a complex thing I must work on internally.

That other lib you linked to, which you've mentioned you're a fan of doesn't have close to half the features Tagify has.

I will make it work, just need to think what would be the best way to tell the component it shouldn't add/remove/edit tags by itself, but only fire a "change" event with the new-to-be value and then it is up to you as the developer to update your state and re-feed the component with the new value.

This won't be as performant as letting tagify handle the DOM because it knows best only to update what's needed and not everything entirely every time there's a change, but if this is what you absolutely need..

In my opinion you don't need to be so strict about this and just give up and let Tagify do what it does and you just make sure you keep your state in sync and not the other-way around. What does it matter anyway... it's not like it could get out-of-sync.

You can always change the value of your tagify component and re-sync it to whatever state you wish. The React demo (code starts after line 55) shows the "state" being fed-back as a new value

@agusterodin
Copy link
Author

That would be awesome

@agusterodin
Copy link
Author

Thanks kind sir

@agusterodin
Copy link
Author

agusterodin commented May 16, 2020

I am familiar with difference between a library that was built in react vs. a library that is built in vanilla that uses a wrapper to interact with the underlying javascript object in response to changes in props.

I think similar to the library I mentioned (which admittedly does have a lot fewer features) would be to have

  • onAdd callback to tell you what single tag that was just attempted to be added (but don't actually add the tag to the underlying vanilla javascript instance). The user will then use this information to determine how (or if) they want to modify the tag state (the thing that gets passed into the 'tags' prop) on their own. Once they modify this on their own it will be reflected via the wrapper logic using loadOriginalValues behind the scenes)

  • similarly, onRemove callback to tell you the index of the single tag that was just attempted to be remove (but don't actually remove the tag to the underlying vanilla javascript instance). The user will then use this information to determine how (or if) they want to modify the tag state (the thing that gets passed into the 'tags' prop) on their own. Once they modify this on their own it will be reflected via the wrapper logic using loadOriginalValues behind the scenes)

  • similarly, onChange callback to tell you the index of the single tag that was just attempted to be changed along with the new value that it was attempted to be changed to (but don't actually change the tag to the underlying vanilla javascript instance). The user will then use this information to determine how (or if) they want to modify the tag state (the thing that gets passed into the 'tags' prop) on their own. Once they modify this on their own it will be reflected via the wrapper logic using loadOriginalValues behind the scenes)

The issue I see with just providing the all values via the onChanged method versus providing separate onAdd/onRemove/onChanged is that the person that is going to mutate the state themselves would likely want to know exactly how the input should change vs. just knowing what it would change to if tagify instance were to do its stock behavior. My particular use case would greatly benefit from onAdd/onRemove versus just using onChange.

I understand that the 'controlled' behavior would potentially big changes to your core tagify library and the stuff I am saying is a lot easier said than done. The best way I see this being done is having an "option" for controlled mode that will get passed in as an extra when we initialize tagify from our react wrapper (this thing: new Tagify(inputRef.current, { callbacks, ...options } || {})). This will inform the core library that the instance of tagify that we just created will be self managed.

As you mentioned, loadOriginalValues is not bad at all! The way native React libraries mutate the DOM vs. the way loadOriginalValues just recreates all of the DOM elements is completely unnoticable as far as performance goes in this scenario of not :) I fully understand that the only way to get this mutating behavior would be to rewrite the library in pure react which is completely impractical and for virtually no benefit.

@agusterodin agusterodin changed the title Proper controlled React wrapper Controlled component behavior in React wrapper May 16, 2020
@agusterodin
Copy link
Author

agusterodin commented May 16, 2020

As far as a use case for controlled behavior vs uncontrolled behavior in the context of using this library:

import React, { useRef } from 'react'
import { connect } from 'react-redux'
import Tags from '../Shared/TagifyWrapper'
import { setTags } from 'Redux/State/upload'
import { setToast } from 'Redux/State/general'
import { usePrevious } from 'Components/Shared/Hooks'
import { TAG_LENGTH_CRITERIA_ERROR } from 'errorHandling'
import { TAG_COUNT_CRITERIA_ERROR, TAG_UNIQUE_CRITERIA_ERROR } from '../../errorHandling'

const mapStateToProps = state => ({ uploadedImage: state.upload.uploadedImage })

const mapDispatchToProps = { setTags, setToast }

const TagifyInput = ({ uploadedImage, setTags, setToast }) => {
  const NO_TAG_PLACEHOLDER = 'Tags (Space Seperated)'
  const TAG_PLACEHOLDER = 'Tag'

  const tagifyRef = useRef()
  const previousImage = usePrevious(uploadedImage)

  const addHandler = event => {
    deleteOverlyLongTag(event)
    deleteExcessTag(event)
    deleteDuplicateTag(event)
    updateTagState()
    updatePlaceholder()
  }

  const editHandler = event => {
    deleteOverlyLongTag(event)
    updateTagState()
  }

  const removeHandler = event => {
    updatePlaceholder()
    updateTagState()
  }

  const updateTagState = () => {
    setTags(
      tagifyRef.current.value.map(current => {
        return current.value
      })
    )
  }

  const updatePlaceholder = () => {
    const tags = tagifyRef.current.value
    const textboxNode = tagifyRef.current.DOM.input
    if (tags.length === 0) {
      textboxNode.setAttribute('data-placeholder', NO_TAG_PLACEHOLDER)
      textboxNode.classList.remove('fit-width')
    }
    else {
      textboxNode.setAttribute('data-placeholder', TAG_PLACEHOLDER)
      textboxNode.classList.add('fit-width')
    }
  }

  const deleteOverlyLongTag = event => {
    if (event.detail.data.value.length > 15) {
      tagifyRef.current.removeTag(event.detail.tag)
      setToast({ text: TAG_LENGTH_CRITERIA_ERROR, style: 'error' })
    }
  }

  const deleteExcessTag = event => {
    if (event.detail.index > 4) {
      tagifyRef.current.removeTag(event.detail.tag)
      setToast({ text: TAG_COUNT_CRITERIA_ERROR, style: 'error' })
    }
  }
  const deleteDuplicateTag = event => {
    const duplicateTags = tagifyRef.current.value.filter((current, index) => {
      return current.value === event.detail.data.value && index !== event.detail.index
    })
    if (duplicateTags.length !== 0) {
      tagifyRef.current.removeTag(event.detail.tag)
      setToast({ text: TAG_UNIQUE_CRITERIA_ERROR, style: 'error' })
    }
  }

  // Remove all tags if image gets changed (ie form gets entirely reset)
  if (previousImage && previousImage !== uploadedImage) {
    tagifyRef.current.removeAllTags()
    updatePlaceholder()
  }

  return (
    <Tags
      tagifyRef={tagifyRef}
      className="form-textbox"
      delimiters=" "
      placeholder={NO_TAG_PLACEHOLDER}
      duplicates={true}
      addTagOnBlur={false}
      editTags={false}
      onAdd={addHandler}
      onEdit={editHandler}
      onRemove={removeHandler}
      onBlur={() => {
        tagifyRef.current.DOM.input.innerHTML = ''
        updatePlaceholder()
      }}
    />
  )
}

export default connect(mapStateToProps, mapDispatchToProps)(TagifyInput)

Pardon my bad code.

I want to display a toast when the user attempts to add a tag that goes beyond the limit or has too many characters. To achieve this, I leave the the maxTags property blank. With controlled behavior when a tag is attempted to get added I would be able to reject it (don't add to single source of truth) and show a toast. Ideally since I'm using redux the onAdd event would trigger an action which would then get received and processed by redux-saga. The saga would determine if the new entry that was attempted to get added wouldn't cause me to exceed the maximum number of tags and if it doesn't exceed the maximum number of characters allowed inside a tag. If too many tags I want to dispatch an action from redux to display a toast with a "too many tags" text, if tag is too long in character length I want to dispatch a toast with a "tags must be x length" message, or if tag successfully fits all criteria I want then to dispatch action to redux to add to the tags state array. Giving me just a list of all tags through the onChange event limits me into being able to distinguish where the tag was added. I understand that you have a maxCharacters and maxTag attribute but then using these puts you at the mercy of your core library behavior. Even worse, if you wanted to implement something even more custom (like say you only want to allow tags to contain certain characters), then you would be completely out of luck whereas having the information to make these decisions yourself and a controlled component would allow infinitely customizable behavior for react folks that need it with no need to add on to your core library.

Note that the wrapper could also benefit from being able to detect changes to the placeholder and apply them accordingly. This would allow me to pass
placeholder={tags.length === 0 ? NO_TAG_PLACEHOLDER : TAG_PLACEHOLDER} and get rid of a lot of code where I do it manually. Would that be worth adding to the issue tracker as a separate issue?

Attached is the behavior that I try to achieve with the above code. Debatably toasts are not the best UX for this scenario but you see how this could apply to other people's use cases as well. It does work but could be much more streamlined if there was controlled behavior: https://streamable.com/gqqoyv

@yairEO
Copy link
Owner

yairEO commented May 16, 2020

Tagify now supports hooks, not React hooks, but "hooks" as the term was originally used before React or any other framework existed. Developers used to name certain placed in their code where other developers could intervene and chose how to proceed and in what manner.

I've only made a "beforeRemoveTag" hook, but the demo clearly shows how it can be used to stop Tagify from removing a tag.

I actually did code an "beforeAddTag" hook but have decided to delete it from the code because I couldn't think of a use-case scenario.

You should not forget tags may be editable, so you also need an "onBeforeEditDone" hook, so this means now you will need to listen to 3 hooks (add, remove, edit) to change manually sync the state and re-feed Tagify with new value (to force it to be "controlled").

Also, you must take into consideration that Taigy has other modes: "mix" & "select" for which you should also address when making a "controlled" component, because I do not want to release a version which only supports "regular" tags as "controlled".

Therefore I have concluded that developers should have a simple "channel" of communication with Tagify through onChange event listener, which I assumed already exists in their current tags (if they wish to move to using Tagify).

onChange listener is mode-agnostic and it's up to the developer to infer what was changed and update their state. For regular more, devs only need to sync the tagify.value Array collection, and for "mix" mode, devs should use the value of the textarea itself (which is "encoded" as stringified tags mixed with "normal" text)

Now, if you want a "controlled" component, then I think the best option is to pass this desire in Tagify's "settings", as a boolean, and this new setting will internally stop any DOM modification done by Tagify, but will fire a change event with the value that "should" have been rendered, and then the React developer could use that value to update their Store and re-feed it to tagify.

This is all in in theory, I'm not even sure it's fully feasible but I can try, when I get to it...

@yairEO
Copy link
Owner

yairEO commented May 16, 2020

I want to display a toast when the user attempts to add a tag that goes beyond the limit or has too many characters. To achieve this, I leave the the maxTags property blank. With controlled behavior when a tag is attempted to get added I would be able to reject it (don't add to single source of truth) and show a toast.

Tagify fires an invalid event which you can check in case a user did anything which isn't allowed by you. It tells you exactly the reason of the invalidation.

Invalid tags are only rendered but not added to the tagify.value array not to the original (hidden) input element.

tagify.on('invalid', console.log)

Ideally since I'm using redux the onAdd event would trigger an action which would then get received and processed by redux-saga. The saga would determine if the new entry that was attempted to get added wouldn't cause me to exceed the maximum number of tags and if it doesn't exceed the maximum number of characters allowed inside a tag.

As a UX/Product person, I do not like the idea of a user adding a tag and then wait an arbitrary amount of time before seeing it added. A server request might be slow and the user will be left in the dark, or worse, try to add the same tag again.
Therefore the tag should be added ASAP and only then update the server with the new value, and in case of failure, remove that tag and notify the user something went wrong.
You can also add a loader to the tag, to indicate something is going on behind-the-scenes:

tagify.on('add', ({detail:tag}) => {
	tagify.tagLoading(tag)
    ...do ajax...
    // if all good
    tagify.tagLoading(tag, false)
    // if server failed to update
    tagify.removeTags(tag)
    ...show a toaster message...
})

If too many tags I want to dispatch an action from redux to display a toast with a "too many tags" text, if tag is too long in character length I want to dispatch a toast with a "tags must be x length" message, or if tag successfully fits all criteria I want then to dispatch action to redux to add to the tags state array. Giving me just a list of all tags through the onChange event limits me into being able to distinguish where the tag was added. I understand that you have a maxCharacters and maxTag attribute but then using these puts you at the mercy of your core library behavior. Even worse, if you wanted to implement something even more custom (like say you only want to allow tags to contain certain characters), then you would be completely out of luck whereas having the information to make these decisions yourself and a controlled component would allow infinitely customizable behavior for react folks that need it with no need to add on to your core library.

Tagify doesn't have maxCharacters, and it's actually extremely powerful in what rules you can apply to anything without ever modifying the source code.
You have pattern for regex tag validations for starters, which gives a ton of control. you also have the ability to transformTag not to mention many other settings..for every possible scenario.

Please I want to know a scenario which tagify cannot handle, please let me know of any.

@agusterodin
Copy link
Author

agusterodin commented May 16, 2020

Tying into your hooks is a pretty solid approach. If you had beforeAdd, beforeEdit, and beforeDelete hooks you could have it so the react wrapper exposes these hooks as -> onAdd, onEdit, and onDelete. The wrapper would trigger the passed in onAdd, onEdit, or onDelete callback the user provided while providing their callback function with all appropriate information like proposed value, index, etc etc, and always immediately provide the beforeX hook with a return value of false value so it doesn't actually add it to the dom elements directly from the underlying tagify instance. If this performs well in real life usage and there are no weird visual glitches it would achieve a very react-like controlled experience with minimal modification required to the code.

Also here is the flush updates warning I was talking about. It happens on your crazy tags demo:

Screen Shot 2020-05-16 at 5 55 43 PM

In general I hear you loud and clear about your library having lots of built in functionality like invalid event, etc. I just think in general it would be cool to have full control over these tags and treat it as a 'dumb' component instead of using all the built in functionality though, especially since it lets you decouple some of your logic from this particular tag library and lets you test aforementioned custom logic very easily.

@yairEO
Copy link
Owner

yairEO commented May 30, 2020

I've also seen these can not flush updates while react is already rendering but am unable to pin-point their exact origin in the code.
Now I am unable to reproduce a scenario which logs this error. weird.

@josejulio
Copy link

Now I am unable to reproduce a scenario which logs this error. weird.

FWIW I was seeing those weird errors locally, but it looks like they are gone after upgrading from 3.9.3 to 3.20.3

@Spoonrad
Copy link

I'm using a standard controlled component (I think ;) ) implementation where the parent component passes values as props, and the child component uses <Tags, passing the props value and onChange. onChange is a callback to the parent, which changes its state.value and passes state.value as props to the child component.

This leads to infinite callbacks.
Sandbox
Just uncomment the lines in the Parent.jsx file

Is this related to this issue? I don't have as much React / front-end expertise as you guys so I haven't fully followed this discussion.

@josejulio
Copy link

@Spoonrad The problem that I see is that every time you change the value, a new value is passed to Child, and that triggers an onChange (which repeats).

I'm not exactly sure what would be the best way to handle, but one option would be to compare current value with what's received If it's the same, don't change the state.

Also, the "value" provided seems to be the same for every call (e.g. it mutates the value contents without creating a new one) so you have to do some depth comparison (not a shallow one).

This compare logic would be better the Child, so maybe if the value is the same, don't post an onChange. If it changes, I would suppose it would be good to create a new value out of the contents of the other. To allow shallow comparison to consumers this.props.onChange({...tagifyInstance.value}). This would require keeping an state on the Child to keep track of previous values.

@Spoonrad
Copy link

I use this implementation with my antd components, which are derived off react-components, so I was just a little bit confused about why this was happening.

It's not a very big deal, I get the initial values as props in Child, pass them to the Child's state in its constructor and the Parent's state gets updated when a change happens. It still passes the value as props but that is only used once, in the Child's constructor.

So it works, it's just not super recommended by React AFAIK.

@yairEO
Copy link
Owner

yairEO commented Mar 26, 2021

I can internally do a deep-compare in the React wrapper and only proceed if new state is different

  1. React Tagify doesn't need to be a "controlled" component, although it can be, I cannot think of a scenario it should be controlled.

  2. @Spoonrad - Why are you still writing old-style react instead of functional React? I would advise to not write in this manner and start using hooks, they have been the norm for 3 years already

yairEO pushed a commit that referenced this issue Mar 28, 2021
@Spoonrad
Copy link

Spoonrad commented Mar 29, 2021

@yairEO I haven't found any particular benefit to using hooks, except the minor benefit that it avoids component injection but that's about it. I don't really see the point of learning & migrating a new syntax if it doesn't have any benefit, and there is no planned deprecation of standard components

Just looked at some articles and I can see why hooks are useful, but their usage doesn't really respond to a need currently. We'll look into it, thanks for the feedback. :)

@yairEO
Copy link
Owner

yairEO commented Mar 29, 2021

One benefit is others will be easily able to read your code. I used to work with React classes before hooks and now I don't remember much at all, after not seeing class components for over 3 years.

React Hooks are so much more elegant IMHO, I love them so so much.

Anyway, I've pushed a fix to the Tagify React wrapper so new values will be compared to current ones and only if different, should render the new ones

@Spoonrad
Copy link

Spoonrad commented Mar 29, 2021

I can definitely think of scenarios where you want the component to be controlled by the way. I'm trying to set a form up that contains an input with Tags and it's been a nightmare. I'm still stuck with infinite loops, even if the value props never changes. For some reason, adding an item + the onChange callback + the value props (which never changes) leads to the infinite loop. The only solution I can think of to solve my problem is to add a ref to my child component and for the parent to get the data through the ref instead of using onChange.

I thought that making sure to always pass the same value in props would fix the issue but nope :/

I've updated to 3.25 and no change. The sandbox example posted above doesn't work either with 3.25

@yairEO
Copy link
Owner

yairEO commented Mar 29, 2021

@Spoonrad - what are the steps I need to do in order to reproduce the issue?

Currently the onChange in Parent.jsx looks like this:

onChange = (value) => {
    // Uncommenting this
    // makes everything break
    // with infinite callbacks
    this.setState({
      value: value,
      xxx: 'c'
    });
  };

I'm adding/removing tags and don't experience any infinite loop

I can definitely think of scenarios where you want the component to be controlled by the way

I would be happy to hear.

Tagify handles its own DOM, it will add/remove/edit nodes by itself and all a developer needs to do is listen to the change event and, if wished, save the state on a remote server. Having a controlled component means that at some point, after Tagify has already modified the DOM and the changes have been rendered, the developer wished to manipulate the state in some manner and re-feed the new modified state back to Tagify

@Spoonrad
Copy link

Spoonrad commented Mar 29, 2021

Typically what I'm used to doing in a form is the implementation in the sandbox (sorry I just edited it so the loop can happen again). A parent maintains an object state and has form inputs as its children. It passes the state as props to the children, which then pass the changed value back to the parent, which changes its state.

I'm used to doing things this way and right now I'm struggling a little with Tagify because of the issues with onChange. I'm probably going to stop trying to use onChange and instead get the values by using a ref, but that's usually not a recommended implementation.

@agusterodin
Copy link
Author

Same here. I did this because I wanted to use my own business logic. It seems like this library doesn't want you to use it in a controlled fashion because it really wants you to rely on its own business logic implementations

@yairEO
Copy link
Owner

yairEO commented Mar 29, 2021

@agusterodin - This is a vanilla library that has a simple React wrapper to easily integrate Tagify within JSX.
Have you ever written a React wrapper to a vanilla library? There are practical limitations. It is not as you say, that I actively chose not to allow a controlled component, it is just not feasible to really be controlled all-the-way. Below is a link of how it behaves as a fake "controlled" when in fact the DOM is changed so fast that the user does not notice that it is not a controlled component. For most purposed it wouldn't matter.

I have used Tagify myself, in very large scale, highly complex, React systems, without any issue.


@Spoonrad - it is not Tagify's fault here but your own code. You are setting the state in a way which is forbidden in React - mutating it and not creating a completely new Object on every state change, this is why Tagify does not register the changes of the props:

onChange = (value) => {
    this.setState({
      value: value  // never do this
    });
  };

You are simply pointing the state back to Tagify's internal state. React cannot compare the previous state with the new one in this way, since the previous once also was point to the *exact same Object! This is a crucial fundamental React thing right here.

Another problem with your code is that you did not use my latest version at all, but used some local file instead. See your import declaration for the Tags component - it's simply pointing to a local file in your demo, which is very much not the same as the latest in NPM:

import Tags from "./tagify/react.tagify"; // you use this
import Tags from "@yaireo/tagify/dist/react.tagify"; // when you need to use this

There are many other issues with your code, I had it simplified and here, everything works, and the component is in a controlled manner.

Here's the demo:
https://codesandbox.io/s/tagify-react-wrapper-forked-0ggbx?file=/src/Child.jsx

@Spoonrad
Copy link

Spoonrad commented Mar 29, 2021

@yairEO

Thanks a lot for the feedback and help!

Another problem with your code is that you did not use my latest version at all, but used some local file instead. See your import declaration for the Tags component - it's simply pointing to a local file in your demo, which is very much not the same as the latest in NPM

Just please note that I started off your CrazyTags sandbox and didn't pay attention to the import statement, I just changed the dependency version

Your demo works great! You have been super helpful and I am very grateful for your help. Could you please explain to me what is wrong with the slightly modified code in this demo?
https://codesandbox.io/s/tagify-react-wrapper-forked-19nbq?file=/src/Child.jsx

The transformation here isn't useful but basically the object in the Parent state is different from what is passed to Child (in my real-life example), so I'm trying to figure out why it doesn't work in the case above. Thanks again for the help / feedback.

BTW your demo works fine without the shallow cloning, what fixed it was the version change.

With older version:
https://codesandbox.io/s/tagify-react-wrapper-forked-l68s1

With newer version:
https://codesandbox.io/s/tagify-react-wrapper-forked-fw91f?file=/src/Parent.jsx

@agusterodin
Copy link
Author

Have you ever written a React wrapper to a vanilla library? There are practical limitations. It is not as you say, that I actively chose not to allow a controlled component, it is just not feasible to really be controlled all-the-way. Below is a link of how it behaves as a fake "controlled" when in fact the DOM is changed so fast that the user does not notice that it is not a controlled component. For most purposed it wouldn't matter.

Yes, I have.

@yairEO
Copy link
Owner

yairEO commented Mar 30, 2021

@agusterodin - then you must know that it's impossible to change already-exiting code of the vanilla script from the wrapper itself, therefore the wrapper can only play by the rules of the script it wraps, which means the wrapper is not in charge of the render process, which finally means there's no real way to make the component truly "Controlled". All the wrapper can do is send commands to Tagify, one-way communication.

@Spoonrad
Copy link

@yairEO Sorry for the second ping but since you reacted with 🎉 to my message I wanted to make sure 😁

I'm still stuck with this "real-life" example:
https://codesandbox.io/s/tagify-react-wrapper-forked-19nbq?file=/src/Child.jsx

Basically I need to do some transformations from Parent to Child and from Child to Parent, so I'm trying to pass lists of objects that have the same data structure as Tags; so Parent sends [{value, ...}] and Child sends back [{value, ...}] (in this example the structure is the same). What seems to make things break is when Child passes [{value, ...}] instead of tagsInstance.value, even though you would expect it to work.

@yairEO
Copy link
Owner

yairEO commented Mar 30, 2021

@Spoonrad

Your demo works great! You have been super helpful and I am very grateful for your help. Could you please explain to me what is wrong with the slightly modified code in this demo?
codesandbox.io/s/tagify-react-wrapper-forked-19nbq?file=/src/Child.jsx

Child.jsx

onChange(
      tagifyInstance.value.map((tag) => {
        return {
          value: tag.value
        };
      })
    );

Child is creating a new Object from Tagify's value and that value is then goes to the Parent, which again feeds it back to the Child, but when tagify gets the new value, it mutates it, adding __isValid property, which was removed by you, to every tag data. This means when the wrapper compares current value with new value (which you just forced it it because it is now a controlled component) it sees the new value is different from the current (remember that the current has been modified inside Tagify due to internal concerns), so that means the Tagify wrapper will "allow" to load the new value, which in turn will get transformed with an __isValid property, and the cycle will endlessly go on.

I am working on a fix to the comparison code within the React Wrapper to not compare new value with current tagify.value because there is a high probability for them to different, but instead, to compare to the original's input value tagify.DOM.originalInput, which is the ultimate source of truth.


BTW your demo works fine without the shallow cloning, what fixed it was the version change.
regardless, you should create a new state object every time and not rely that that Tagify itself, may or may not, does that internally

yairEO pushed a commit that referenced this issue Mar 30, 2021
@yairEO
Copy link
Owner

yairEO commented Mar 30, 2021

@Spoonrad - I have worked for the past hours to help you in your case and improved some internal mechanism in Tagify and have published a new version 4.0.0 because I had to introduce a breaking change in the way the onChange works in the React wrapper.

Here's the demo:
https://codesandbox.io/s/tagify-react-wrapper-forked-4blwl?file=/src/Child.jsx

@Spoonrad
Copy link

@yairEO Thanks for all your quick work on this, definitely appreciate it! I'm going to look into this new version very soon and let you know if I was able to solve my problem. Does this mean that now it is best not to use the tagifyInstance.value but rather parse the value passed through event.detail?

@yairEO
Copy link
Owner

yairEO commented Mar 30, 2021

@Spoonrad

tagifyInstance.value has properties that are automatically added to it, which you might remove when saving it to your state, and then, when you re-feed the component with the "new" value prop, it will not have those properties and the wrapper will think that the value has indeed changed, and will cause a chain of events that in the end will again call the change event, and all of what I just said will happen again endlessly.

I have introduced a new method to tagify, called getCleanValue() which returns the value, without any added properties, as an Array.

The event.detail.value of the change event listener callback is a string, and should represent the same string as the value of the "original" input element (there's a hidden input element after the tagify component). This value is of type String and represent an Array which can be parsed using JSON.parse()

@Spoonrad
Copy link

Spoonrad commented Mar 30, 2021

OK, it works well with getCleanValue(). Note that I also need to control the color so it's a little bit tricky.
I pass a list of objects with structure {value, color_key, style} where style is --tag-bg: xxx
When a new tag is added, I don't have its color yet, so I pass the new tag to the Parent using getCleanValue(), the Parent passes it back to the Child, which creates new objects with the color_key and style and passes it to Tags as props.
Passing color_key, which is used to identify the color value, as props to the value of a Tag, by the way, is useful because it avoids having to search through the options and find the color key when an onChange event occurs.

With JSON.parse() it doesn't work (the tags are reset when you leave the input box) but with getCleanValue() it does.

I have the following warning though:

Warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
    at input
    at div
    at Tags (http://127.0.0.1:3000/static/js/vendors~main.chunk.js:86398:18)

@yairEO
Copy link
Owner

yairEO commented Mar 30, 2021

I will fix this warning.

What do you mean by:

When a new tag is added, I don't have its color yet

Does the color comes from some back-end?

If not, then it's better to add the color through the Tagify "proper" way, using the transformTag setting, which is shown in this elaborate demo:

https://codepen.io/vsync/pen/bGpdVOr

@Spoonrad
Copy link

Spoonrad commented Mar 30, 2021 via email

@ogrodev
Copy link

ogrodev commented Apr 2, 2021

I'm getting the warning as mentioned by @Spoonrad 2msgs above.
@yairEO you commented that would fix this warning, but then @Spoonrad said everything seems to work now and I'm really confused if the warning was fixed or not.

Installed the dependency with NPM today. Component code below:

import React, { useCallback } from 'react'
import Tags from "@yaireo/tagify/dist/react.tagify"
import "@yaireo/tagify/dist/tagify.css"
import styles from '../../styles/TagifyInput.module.css'


export default function TagifyInput({ tags, setTags }) {

    const onAdd = (e) => {
        console.log(e)
    }

    return(
        <Tags
        settings={{delimiters: ";"}}
        placeholder='Insert team tags'
        className={styles.customLook}
        onAdd={onAdd}
        />
    )
}

I'm also having another problem, I'm having to click on the input field of Tags exactly 3 times before I could maintain focus :(

@Spoonrad
Copy link

Spoonrad commented Apr 2, 2021

I don’t think the warning is there in my project anymore, I don’t believe I have seen it again but I may be wrong. I’m making a quick implementation because I need something testable quickly at the moment.

@yairEO
Copy link
Owner

yairEO commented Apr 4, 2021

Hi @sweetsoul, if you think Tagify has a bug and you can reproduce the bug in a demo page, then please open a new issue

@agrawal-rohit
Copy link

The problem

A bit late to the party, but I'm facing an unusual issue as well when trying to emulate controlled behavior with the React wrapper. I maintain two parallel mix-mode tagify components at a time, each with their individual refs, and I need to share the value between them. I have the single source of truth in a store which I pass as the value prop to each component, and when any onChange event gets fired from either of the components, I directly update the store value.

Now, I am able to share the state successfully between the components, but, I noticed that whenever I add a new tag - the caret position gets sent to the beginning. If I remove the value prop and keep the components uncontrolled, then the caret position doesn't get affected.

You can test out this behaviour in this CodeSandbox

Solution?

After checking out the react.tagify.js file, I assume that this issue is occurring due to the following hook:

useEffect(() => {
  const currentValue = tagify.current.getInputValue()

  if (mountedRef.current && !isSameDeep(value, currentValue)) {
       tagify.current.loadOriginalValues(value)
   }
}, [value])

Every time the loadOriginalValues method is being invoked, I'm assuming the caret position is being reset to the start.

@yairEO thoughts?

@yairEO
Copy link
Owner

yairEO commented Apr 22, 2022

@agrawal-rohit - you can just not update the value prop of the currently-active Tagify and bypass the issue entirely since the caret position will not change

@agrawal-rohit
Copy link

@yairEO, that's interesting. Would it be possible for you to update this CodeSandbox with a possible solution? It'd be a huge help 😇

@yairEO
Copy link
Owner

yairEO commented May 5, 2022

@agrawal-rohit - sorry but I'm swamped with work at the moment and it will take me days to get to it..

@agrawal-rohit
Copy link

That's fine, It's not super urgent 😇

@agrawal-rohit
Copy link

@yairEO could you add a solution in the sandbox if you get some time?

@faulty13
Copy link

faulty13 commented Aug 30, 2024

@yairEO - really liking Tagify so far!

I have very little React experience, so apologies in advance if the question below is a bit silly. I've put my question here, because I'm not sure if it's related to controlled component behaviour.

I'm currently using the React version, by copying the MixedTags example here. It seems like OnChange only triggers when a tag is selected. How would I go about making OnChange trigger for something like OnKeyPress as well?

The reason I'm looking to do this is so that I can read in the current value at all times (for a Plotly Dash app callback), not just after selecting a tag.

Currently logging looks like the below:
CHANGED: "[[{\"id\":1,\"value\":\"tag1\",\"title\":\"tag1\",\"prefix\":\"@\"}]]"
CHANGED: "[[{\"id\":1,\"value\":\"tag1\",\"title\":\"tag1\",\"prefix\":\"@\"}]] is smaller than [[{\"id\":2,\"value\":\"tag2\",\"prefix\":\"@\"}]]"
rather than letter by letter.

This poses an issue if I were to stop at "@tag1 is small", because the last changed value would still look like the first logging above.

@yairEO
Copy link
Owner

yairEO commented Aug 30, 2024

@faulty13 - there are many events you can use: onKeydown, onInput... it's all in the README:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants