Conversation
| --- | ||
| category: forms | ||
| title: Add Grouped Radio Buttons with Custom Toolbar Button | ||
| description: Create a custom toolbar item that programmatically places pre-grouped radio buttons with a single click, avoiding manual renaming. |
There was a problem hiding this comment.
what does pre-grouped means here?
There was a problem hiding this comment.
It means radio buttons are created with the same formFieldName, so they automatically function as a group
There was a problem hiding this comment.
aren't they called grouped?
| defaultValue: "1", | ||
| }, | ||
| ); | ||
| await instance!.create([radioWidget1, radioWidget2, formField]); |
There was a problem hiding this comment.
The ! here is the non-null assertion operator—it tells TypeScript "trust me, this isn't null." Since instance is typed as Instance | null, what's the intended behavior if it actually is null at this point? Should it:
- Fail loudly (current behavior with ! if null at runtime)
- Silently skip the call (use
?.) - Something else (explicit check with error message?)
There was a problem hiding this comment.
I already worked on something similar btw: #155
There was a problem hiding this comment.
@veroo-m , if I correctly understood your comment, I've added an if (!instance) return. Is that right?
There was a problem hiding this comment.
@eli7pm , I see that yours auto-groups them widgets during manual creation in Form Creator mode, while mine provides a toolbar button for programmatic placement of grouped radio buttons. I was reluctant at first to close this one, but I think they're both useful for different workflows + this one adapts Rahul's new Playground structure.
There was a problem hiding this comment.
I would use await instance?.create([radioWidget1, radioWidget2, formField]);
|
Tested with Miguel's project to test and it seems to be working |
|
@veroo-m , could you please review this? |
veroo-m
left a comment
There was a problem hiding this comment.
good job. I added a comment to improve the readability of the code. After the change it can be merged.
| onPress: async () => { | ||
| const radioWidget1 = new window.NutrientViewer.Annotations.WidgetAnnotation( | ||
| { | ||
| id: window.NutrientViewer.generateInstantId(), | ||
| pageIndex: 0, | ||
| formFieldName: "MyFormField", | ||
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | ||
| left: 100, | ||
| top: 100, | ||
| width: 20, | ||
| height: 20, | ||
| }), | ||
| }, | ||
| ); | ||
|
|
||
| const radioWidget2 = new window.NutrientViewer.Annotations.WidgetAnnotation( | ||
| { | ||
| id: window.NutrientViewer.generateInstantId(), | ||
| pageIndex: 0, | ||
| formFieldName: "MyFormField", | ||
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | ||
| left: 130, | ||
| top: 100, | ||
| width: 20, | ||
| height: 20, | ||
| }), | ||
| }, | ||
| ); | ||
|
|
||
| const formField = new window.NutrientViewer.FormFields.RadioButtonFormField( | ||
| { | ||
| name: "MyFormField", | ||
| annotationIds: new window.NutrientViewer.Immutable.List([ | ||
| radioWidget1.id, | ||
| radioWidget2.id, | ||
| ]), | ||
| options: new window.NutrientViewer.Immutable.List([ | ||
| new window.NutrientViewer.FormOption({ | ||
| label: "Option 1", | ||
| value: "1", | ||
| }), | ||
| new window.NutrientViewer.FormOption({ | ||
| label: "Option 2", | ||
| value: "2", | ||
| }), | ||
| ]), | ||
| defaultValue: "1", | ||
| }, | ||
| ); | ||
|
|
||
| await instance?.create([radioWidget1, radioWidget2, formField]); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
for readability please move defining and creating the radio widgets and associated form field to a function that gets called inside the onPress(). Smtg like:
| onPress: async () => { | |
| const radioWidget1 = new window.NutrientViewer.Annotations.WidgetAnnotation( | |
| { | |
| id: window.NutrientViewer.generateInstantId(), | |
| pageIndex: 0, | |
| formFieldName: "MyFormField", | |
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | |
| left: 100, | |
| top: 100, | |
| width: 20, | |
| height: 20, | |
| }), | |
| }, | |
| ); | |
| const radioWidget2 = new window.NutrientViewer.Annotations.WidgetAnnotation( | |
| { | |
| id: window.NutrientViewer.generateInstantId(), | |
| pageIndex: 0, | |
| formFieldName: "MyFormField", | |
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | |
| left: 130, | |
| top: 100, | |
| width: 20, | |
| height: 20, | |
| }), | |
| }, | |
| ); | |
| const formField = new window.NutrientViewer.FormFields.RadioButtonFormField( | |
| { | |
| name: "MyFormField", | |
| annotationIds: new window.NutrientViewer.Immutable.List([ | |
| radioWidget1.id, | |
| radioWidget2.id, | |
| ]), | |
| options: new window.NutrientViewer.Immutable.List([ | |
| new window.NutrientViewer.FormOption({ | |
| label: "Option 1", | |
| value: "1", | |
| }), | |
| new window.NutrientViewer.FormOption({ | |
| label: "Option 2", | |
| value: "2", | |
| }), | |
| ]), | |
| defaultValue: "1", | |
| }, | |
| ); | |
| await instance?.create([radioWidget1, radioWidget2, formField]); | |
| }, | |
| }; | |
| onPress: () => createGroupedRadioButtons(instance) | |
| // .... | |
| const createdGroupedRadioButtons = async (instance) => { | |
| const radioWidget1 = new window.NutrientViewer.Annotations.WidgetAnnotation( | |
| { | |
| id: window.NutrientViewer.generateInstantId(), | |
| pageIndex: 0, | |
| formFieldName: "MyFormField", | |
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | |
| left: 100, | |
| top: 100, | |
| width: 20, | |
| height: 20, | |
| }), | |
| }, | |
| ); | |
| const radioWidget2 = new window.NutrientViewer.Annotations.WidgetAnnotation( | |
| { | |
| id: window.NutrientViewer.generateInstantId(), | |
| pageIndex: 0, | |
| formFieldName: "MyFormField", | |
| boundingBox: new window.NutrientViewer.Geometry.Rect({ | |
| left: 130, | |
| top: 100, | |
| width: 20, | |
| height: 20, | |
| }), | |
| }, | |
| ); | |
| const formField = new window.NutrientViewer.FormFields.RadioButtonFormField( | |
| { | |
| name: "MyFormField", | |
| annotationIds: new window.NutrientViewer.Immutable.List([ | |
| radioWidget1.id, | |
| radioWidget2.id, | |
| ]), | |
| options: new window.NutrientViewer.Immutable.List([ | |
| new window.NutrientViewer.FormOption({ | |
| label: "Option 1", | |
| value: "1", | |
| }), | |
| new window.NutrientViewer.FormOption({ | |
| label: "Option 2", | |
| value: "2", | |
| }), | |
| ]), | |
| defaultValue: "1", | |
| }, | |
| ); | |
| await instance?.create([radioWidget1, radioWidget2, formField]); | |
| }, | |
| } |
Added an explicit if (!instance) return
Updated the description to clarify that radio buttons are grouped.
695678e to
20b7f15
Compare
Thanks for the reminder, @miguelcalderon! Had some CI issues and managed to fix them. Would it be possible to give it a final quick review and let me know if it should be merged or if we have any other changes to implement? |
miguelcalderon
left a comment
There was a problem hiding this comment.
Just a small but key change to the url 🙇
| let instance: Instance | null = null; | ||
|
|
||
| const createGroupedRadioButtons = async (instance: Instance | null) => { | ||
| const radioWidget1 = new window.NutrientViewer.Annotations.WidgetAnnotation({ |
There was a problem hiding this comment.
It should be possible to use NutrientViewer directly without window.NutrientViewer.
| @@ -0,0 +1,2 @@ | |||
| [InternetShortcut] | |||
| URL=https://playground.pspdfkit.com/?p=eyJ2IjoxLCJjc3MiOiIvKiBBZGQgeW91ciBDU1MgaGVyZSAqL1xuIiwic2V0dGluZ3MiOnsiZmlsZU5hbWUiOiJiYXNpYy5wZGYifSwianMiOiJsZXQgaW5zdGFuY2UgPSBudWxsO1xuXG5jb25zdCBpdGVtID0ge1xuICB0eXBlOiBcImN1c3RvbVwiLFxuICBpZDogXCJhZGQtcmFkaW8tZ3JvdXBcIixcbiAgdGl0bGU6IFwiQWRkIFJhZGlvIEdyb3VwXCIsXG4gIG9uUHJlc3M6IGFzeW5jICgpID0%252BIHtcbiAgICBjb25zdCByYWRpb1dpZGdldDEgPSBuZXcgTnV0cmllbnRWaWV3ZXIuQW5ub3RhdGlvbnMuV2lkZ2V0QW5ub3RhdGlvbih7XG4gIGlkOiBOdXRyaWVudFZpZXdlci5nZW5lcmF0ZUluc3RhbnRJZCgpLFxuICBwYWdlSW5kZXg6IDAsXG4gIGZvcm1GaWVsZE5hbWU6IFwiTXlGb3JtRmllbGRcIixcbiAgYm91bmRpbmdCb3g6IG5ldyBOdXRyaWVudFZpZXdlci5HZW9tZXRyeS5SZWN0KHtcbiAgICBsZWZ0OiAxMDAsXG4gICAgdG9wOiAxMDAsXG4gICAgd2lkdGg6IDIwLFxuICAgIGhlaWdodDogMjBcbiAgfSlcbn0pO1xuY29uc3QgcmFkaW9XaWRnZXQyID0gbmV3IE51dHJpZW50Vmlld2VyLkFubm90YXRpb25zLldpZGdldEFubm90YXRpb24oe1xuICBpZDogTnV0cmllbnRWaWV3ZXIuZ2VuZXJhdGVJbnN0YW50SWQoKSxcbiAgcGFnZUluZGV4OiAwLFxuICBmb3JtRmllbGROYW1lOiBcIk15Rm9ybUZpZWxkXCIsXG4gIGJvdW5kaW5nQm94OiBuZXcgTnV0cmllbnRWaWV3ZXIuR2VvbWV0cnkuUmVjdCh7XG4gICAgbGVmdDogMTMwLFxuICAgIHRvcDogMTAwLFxuICAgIHdpZHRoOiAyMCxcbiAgICBoZWlnaHQ6IDIwXG4gIH0pXG59KTtcbmNvbnN0IGZvcm1GaWVsZCA9IG5ldyBOdXRyaWVudFZpZXdlci5Gb3JtRmllbGRzLlJhZGlvQnV0dG9uRm9ybUZpZWxkKHtcbiAgbmFtZTogXCJNeUZvcm1GaWVsZFwiLFxuICBhbm5vdGF0aW9uSWRzOiBuZXcgTnV0cmllbnRWaWV3ZXIuSW1tdXRhYmxlLkxpc3QoW3JhZGlvV2lkZ2V0MS5pZCwgcmFkaW9XaWRnZXQyLmlkXSksXG4gIG9wdGlvbnM6IG5ldyBOdXRyaWVudFZpZXdlci5JbW11dGFibGUuTGlzdChbXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMVwiLCB2YWx1ZTogXCIxXCIgfSksXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMlwiLCB2YWx1ZTogXCIyXCIgfSlcbiAgXSksXG4gIGRlZmF1bHRWYWx1ZTogXCIxXCJcbn0pO1xuYXdhaXQgaW5zdGFuY2UuY3JlYXRlKFtyYWRpb1dpZGdldDEsIHJhZGlvV2lkZ2V0MiwgZm9ybUZpZWxkXSk7XG4gIH1cbn07XG5cblxuXG5cblxuXG5cbk51dHJpZW50Vmlld2VyLmxvYWQoe1xuICAuLi5iYXNlT3B0aW9ucyxcbiAgdGhlbWU6IE51dHJpZW50Vmlld2VyLlRoZW1lLkRBUkssXG4gIHRvb2xiYXJJdGVtczogWy4uLk51dHJpZW50Vmlld2VyLmRlZmF1bHRUb29sYmFySXRlbXMsIHsgdHlwZTogXCJmb3JtLWNyZWF0b3JcIiB9XVxufSkudGhlbigoX2luc3RhbmNlKSA9PiB7XG4gICAgaW5zdGFuY2UgPSBfaW5zdGFuY2U7XG4gICAgaW5zdGFuY2Uuc2V0VG9vbGJhckl0ZW1zKChpdGVtcykgPT4gWy4uLml0ZW1zLCBpdGVtXSk7XG5cbn0pO1xuXHQifQ%253D%253D | |||
There was a problem hiding this comment.
Should use https://www.nutrient.io/demo/sandbox?p= instead.
| URL=https://playground.pspdfkit.com/?p=eyJ2IjoxLCJjc3MiOiIvKiBBZGQgeW91ciBDU1MgaGVyZSAqL1xuIiwic2V0dGluZ3MiOnsiZmlsZU5hbWUiOiJiYXNpYy5wZGYifSwianMiOiJsZXQgaW5zdGFuY2UgPSBudWxsO1xuXG5jb25zdCBpdGVtID0ge1xuICB0eXBlOiBcImN1c3RvbVwiLFxuICBpZDogXCJhZGQtcmFkaW8tZ3JvdXBcIixcbiAgdGl0bGU6IFwiQWRkIFJhZGlvIEdyb3VwXCIsXG4gIG9uUHJlc3M6IGFzeW5jICgpID0%252BIHtcbiAgICBjb25zdCByYWRpb1dpZGdldDEgPSBuZXcgTnV0cmllbnRWaWV3ZXIuQW5ub3RhdGlvbnMuV2lkZ2V0QW5ub3RhdGlvbih7XG4gIGlkOiBOdXRyaWVudFZpZXdlci5nZW5lcmF0ZUluc3RhbnRJZCgpLFxuICBwYWdlSW5kZXg6IDAsXG4gIGZvcm1GaWVsZE5hbWU6IFwiTXlGb3JtRmllbGRcIixcbiAgYm91bmRpbmdCb3g6IG5ldyBOdXRyaWVudFZpZXdlci5HZW9tZXRyeS5SZWN0KHtcbiAgICBsZWZ0OiAxMDAsXG4gICAgdG9wOiAxMDAsXG4gICAgd2lkdGg6IDIwLFxuICAgIGhlaWdodDogMjBcbiAgfSlcbn0pO1xuY29uc3QgcmFkaW9XaWRnZXQyID0gbmV3IE51dHJpZW50Vmlld2VyLkFubm90YXRpb25zLldpZGdldEFubm90YXRpb24oe1xuICBpZDogTnV0cmllbnRWaWV3ZXIuZ2VuZXJhdGVJbnN0YW50SWQoKSxcbiAgcGFnZUluZGV4OiAwLFxuICBmb3JtRmllbGROYW1lOiBcIk15Rm9ybUZpZWxkXCIsXG4gIGJvdW5kaW5nQm94OiBuZXcgTnV0cmllbnRWaWV3ZXIuR2VvbWV0cnkuUmVjdCh7XG4gICAgbGVmdDogMTMwLFxuICAgIHRvcDogMTAwLFxuICAgIHdpZHRoOiAyMCxcbiAgICBoZWlnaHQ6IDIwXG4gIH0pXG59KTtcbmNvbnN0IGZvcm1GaWVsZCA9IG5ldyBOdXRyaWVudFZpZXdlci5Gb3JtRmllbGRzLlJhZGlvQnV0dG9uRm9ybUZpZWxkKHtcbiAgbmFtZTogXCJNeUZvcm1GaWVsZFwiLFxuICBhbm5vdGF0aW9uSWRzOiBuZXcgTnV0cmllbnRWaWV3ZXIuSW1tdXRhYmxlLkxpc3QoW3JhZGlvV2lkZ2V0MS5pZCwgcmFkaW9XaWRnZXQyLmlkXSksXG4gIG9wdGlvbnM6IG5ldyBOdXRyaWVudFZpZXdlci5JbW11dGFibGUuTGlzdChbXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMVwiLCB2YWx1ZTogXCIxXCIgfSksXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMlwiLCB2YWx1ZTogXCIyXCIgfSlcbiAgXSksXG4gIGRlZmF1bHRWYWx1ZTogXCIxXCJcbn0pO1xuYXdhaXQgaW5zdGFuY2UuY3JlYXRlKFtyYWRpb1dpZGdldDEsIHJhZGlvV2lkZ2V0MiwgZm9ybUZpZWxkXSk7XG4gIH1cbn07XG5cblxuXG5cblxuXG5cbk51dHJpZW50Vmlld2VyLmxvYWQoe1xuICAuLi5iYXNlT3B0aW9ucyxcbiAgdGhlbWU6IE51dHJpZW50Vmlld2VyLlRoZW1lLkRBUkssXG4gIHRvb2xiYXJJdGVtczogWy4uLk51dHJpZW50Vmlld2VyLmRlZmF1bHRUb29sYmFySXRlbXMsIHsgdHlwZTogXCJmb3JtLWNyZWF0b3JcIiB9XVxufSkudGhlbigoX2luc3RhbmNlKSA9PiB7XG4gICAgaW5zdGFuY2UgPSBfaW5zdGFuY2U7XG4gICAgaW5zdGFuY2Uuc2V0VG9vbGJhckl0ZW1zKChpdGVtcykgPT4gWy4uLml0ZW1zLCBpdGVtXSk7XG5cbn0pO1xuXHQifQ%253D%253D | |
| URL=https://www.nutrient.io/demo/sandbox?p=eyJ2IjoxLCJjc3MiOiIvKiBBZGQgeW91ciBDU1MgaGVyZSAqL1xuIiwic2V0dGluZ3MiOnsiZmlsZU5hbWUiOiJiYXNpYy5wZGYifSwianMiOiJsZXQgaW5zdGFuY2UgPSBudWxsO1xuXG5jb25zdCBpdGVtID0ge1xuICB0eXBlOiBcImN1c3RvbVwiLFxuICBpZDogXCJhZGQtcmFkaW8tZ3JvdXBcIixcbiAgdGl0bGU6IFwiQWRkIFJhZGlvIEdyb3VwXCIsXG4gIG9uUHJlc3M6IGFzeW5jICgpID0%252BIHtcbiAgICBjb25zdCByYWRpb1dpZGdldDEgPSBuZXcgTnV0cmllbnRWaWV3ZXIuQW5ub3RhdGlvbnMuV2lkZ2V0QW5ub3RhdGlvbih7XG4gIGlkOiBOdXRyaWVudFZpZXdlci5nZW5lcmF0ZUluc3RhbnRJZCgpLFxuICBwYWdlSW5kZXg6IDAsXG4gIGZvcm1GaWVsZE5hbWU6IFwiTXlGb3JtRmllbGRcIixcbiAgYm91bmRpbmdCb3g6IG5ldyBOdXRyaWVudFZpZXdlci5HZW9tZXRyeS5SZWN0KHtcbiAgICBsZWZ0OiAxMDAsXG4gICAgdG9wOiAxMDAsXG4gICAgd2lkdGg6IDIwLFxuICAgIGhlaWdodDogMjBcbiAgfSlcbn0pO1xuY29uc3QgcmFkaW9XaWRnZXQyID0gbmV3IE51dHJpZW50Vmlld2VyLkFubm90YXRpb25zLldpZGdldEFubm90YXRpb24oe1xuICBpZDogTnV0cmllbnRWaWV3ZXIuZ2VuZXJhdGVJbnN0YW50SWQoKSxcbiAgcGFnZUluZGV4OiAwLFxuICBmb3JtRmllbGROYW1lOiBcIk15Rm9ybUZpZWxkXCIsXG4gIGJvdW5kaW5nQm94OiBuZXcgTnV0cmllbnRWaWV3ZXIuR2VvbWV0cnkuUmVjdCh7XG4gICAgbGVmdDogMTMwLFxuICAgIHRvcDogMTAwLFxuICAgIHdpZHRoOiAyMCxcbiAgICBoZWlnaHQ6IDIwXG4gIH0pXG59KTtcbmNvbnN0IGZvcm1GaWVsZCA9IG5ldyBOdXRyaWVudFZpZXdlci5Gb3JtRmllbGRzLlJhZGlvQnV0dG9uRm9ybUZpZWxkKHtcbiAgbmFtZTogXCJNeUZvcm1GaWVsZFwiLFxuICBhbm5vdGF0aW9uSWRzOiBuZXcgTnV0cmllbnRWaWV3ZXIuSW1tdXRhYmxlLkxpc3QoW3JhZGlvV2lkZ2V0MS5pZCwgcmFkaW9XaWRnZXQyLmlkXSksXG4gIG9wdGlvbnM6IG5ldyBOdXRyaWVudFZpZXdlci5JbW11dGFibGUuTGlzdChbXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMVwiLCB2YWx1ZTogXCIxXCIgfSksXG4gICAgbmV3IE51dHJpZW50Vmlld2VyLkZvcm1PcHRpb24oeyBsYWJlbDogXCJPcHRpb24gMlwiLCB2YWx1ZTogXCIyXCIgfSlcbiAgXSksXG4gIGRlZmF1bHRWYWx1ZTogXCIxXCJcbn0pO1xuYXdhaXQgaW5zdGFuY2UuY3JlYXRlKFtyYWRpb1dpZGdldDEsIHJhZGlvV2lkZ2V0MiwgZm9ybUZpZWxkXSk7XG4gIH1cbn07XG5cblxuXG5cblxuXG5cbk51dHJpZW50Vmlld2VyLmxvYWQoe1xuICAuLi5iYXNlT3B0aW9ucyxcbiAgdGhlbWU6IE51dHJpZW50Vmlld2VyLlRoZW1lLkRBUkssXG4gIHRvb2xiYXJJdGVtczogWy4uLk51dHJpZW50Vmlld2VyLmRlZmF1bHRUb29sYmFySXRlbXMsIHsgdHlwZTogXCJmb3JtLWNyZWF0b3JcIiB9XVxufSkudGhlbigoX2luc3RhbmNlKSA9PiB7XG4gICAgaW5zdGFuY2UgPSBfaW5zdGFuY2U7XG4gICAgaW5zdGFuY2Uuc2V0VG9vbGJhckl0ZW1zKChpdGVtcykgPT4gWy4uLml0ZW1zLCBpdGVtXSk7XG5cbn0pO1xuXHQifQ%253D%253D |
Adding a new playground example demonstrating how to create a custom toolbar button that programmatically creates pre-grouped radio buttons with a single click.