Dialogs
As of now, this page is still a stub. In particular, the considerations on when and where to apply Dialogs are still missing. For now, it can be stated that OpenProject employs Dialogs for more purposes than GitHub Primer specifies.
Async dialogs as turbo streams
Primer dialogs need to be rendered and tied to a button, resulting in their content always being present. If you have any non-trivial content that should be displayed in a dialog, you should use the async dialog pattern. To render dialogs asynchronously, you use a Button/Link/IconButton component as a trigger and add the following attributes to it:
tag: :a
so that the component is rendered as a linkhref: link_to_dialog_path
to specify where to request the dialog stream fromdata: { controller: "async-dialog" }
to attach the async-dialog controller to the button (In some cases, depending on the underlying html structure,content_arguments: { data: { controller: "async-dialog" }}
might be needed instead)
<%= render(Primer::Beta::Button.new( tag: :a, href: link_to_dialog_path, data: { controller: "async-dialog" })) do |button| button.with_leading_visual_icon(icon: :edit) "Edit something"end%>
On the Rails controller you wish to render the dialog, you need to respond to the request with the dialog content. The async-controller stimulus controller will ensure that a loading progress bar will be shown on the top of the page.
class TestController < ApplicationControler # include the helper module include OpTurbo::DialogStreamHelper # ... def dialog respond_with_dialog MyDialogComponent.new(work_package: @work_package) endend
For this to work, the OpTurbo::Streamable
module needs to be included in the modal component, along with the OpTurbo::DialogStreamHelper
module in the corresponding controller:
class MyDialogComponent < ApplicationControler # include the module include OpTurbo::Streamable # ...end
Assuming the MyDialogComponent template looks like this:
<%= render(Primer::Alpha::Dialog.new(title: "Edit something", size: :medium_portrait, id: "my-dialog-id")) do |d| d.with_header(variant: :large) d.with_body do # Whatever you want to render here.. end end%>
You can also render the dialog in a manual turbo_stream template:
class TestController < ApplicationControler # include the helper module include OpTurbo::DialogStreamHelper # ... def dialog; endend
The content of dialog.turbo_stream.erb
would then look like this:
<%= turbo_stream.dialog do render(Primer::Alpha::Dialog.new(title: t(:label_history), id: 'meeting-history-dialog')) do |d| d.with_header(variant: :large) d.with_body do render(Activities::DaysComponent.new(events: @events, current_project: @project, activity_page: @activity_page)) end endend%>
The dialog content is then rendered in a turbo stream response to the request using a custom turbo stream action.
The frontend turbo stream dialog
action will take this dialog, append it to the body, and open it immediately.
Once the dialog is closed, the content is removed from the DOM again.
Dialogs with separate forms
We have seen that we want to render the body part of the dialog in a separate component. This will result in the submit buttons being outside of that component that renders a form.
In the following example, the body renders a separate component with a form. You can tell the
submit button to reference the form attribute using form: "id-of-the-form"
. With turbo,
you have to make sure that the data-turbo attribute is also set.
<%= turbo_stream.dialog do render(Primer::Alpha::Dialog.new(title: t(:label_history), id: 'my-dialog')) do |d| d.with_header(variant: :large) d.with_body do render(ComponentThatRendersAForm.new(form_id: "my-dialog-form") end d.with_footer do render(Primer::Beta::Button.new( tag: :button, type: :submit, form: "my-dialog-form" data: { turbo: true } variant: :primary, ) do |button| button.text = t(:button_save) end) end endend%>
Dialogs with form validations
Sometimes a dialog with a form needs validations. If the form is submitted with turbo, the form can be rendered again with the validation errors within the dialog. In order for the dialog to not be closed, the rendering has to happen with an error response code (therefore 4xx or 5xx). Referring to the example above (see "Dialogs with separate forms"), a controller which renders validation errors inside a dialog could look like that:
class TestController < ApplicationControler # include the helper module include OpTurbo::DialogStreamHelper # ... def update if @my_model.save # ... else component = ComponentThatRendersAForm.new(my_model: @my_model) update_via_turbo_stream(component: component, status: :bad_request) end respond_with_turbo_streams endend
Dialogs that should stay open on form submission
Because we normally want to close the dialog when a form is submitted successfully, we have implemented this as the default behaviour. As described in the last section, when the submission returns a 4xx or 5xx status code, the dialog will stay open and display the validation errors and will be closed when the form is submitted successfully.
If you want to keep a dialog open even after a successful form submission, you can use the data-keep-open-on-submit="true"
attribute on the dialog. This will prevent the dialog
from closing even if the form submission is successful.
<%= turbo_stream.dialog do render(Primer::Alpha::Dialog.new(data: { 'keep-open-on-submit': true }) do |d| # ... endend%>
Special kinds of dialogs
We have multiple kind of dialogs that re-appear throughout the whole application. In order to make sure, they all look and feel alike, we further specify them here.
Confirmation dialogs
Whenever a user is about to perform an action that has a significant impact, a confirmation dialog is shown.
This can come in the form of a positive confirmation where a user is asked to e.g. confirm the creation of a new model.
Or it can be to ascertain the user actually wants to destroy a model.
Note: Currently, confirmation dialog require a lot of manual work to be implemented.
Prefer using a native browser confirmation dialog when possible using the data-confirm
attribute and UJS for the time
being.
Primerized confirmation dialogs are still a work in progress.
Where it's used
- Project list → Delete the project list
Form dialogs
Within the OpenProject application, forms are oftentimes rendered within a dialog. It is used e.g. for the creation of new models or for the modification of existing ones.
Form dialogs consist of a header, a body where the form is rendered in and a footer with the buttons to submit or cancel the action.
The primer specification suggests a pattern for rendering such dialogs. It consists of a form spanning both the body as well as the footer of the dialog. This has the drawback of the footer potentially being rendered outside of the dialog. That behaviour is undesirable. Primer defines to cope with longer content by scrolling the body.
In order for this to happen, the form should only be rendered inside the body. The submit button can be logically put inside
the form via the form
attribute.
In pseudo html, the logical format is then like this:
<dialog> <dialog-header> Dialog title </dialog-header> <dialog-body> <form id='the-form-id'> ... form content ... </form> </dialog-body> <dialog-footer> <input type="submit" form="the-form-id">Submit</button></dialog-footer></dialog>
Where it's used
- WorkPackage → Meeting Tab → Add meeting to WorkPackage
User nudging modal
The idea of nudging modals is to promote users to perform actions that are not 100% mandatory but might be interesting for them. For example, after creating a type, an admin would like to add the new type to a project and configure a workflow for it. Another example is to grant personal access to a recently created storage:
Accessibility considerations
To be written down..
Where it's used
- Admin → Storages → OAuth Grant Access