├── .nojekyll
├── .gitignore
├── docs
├── styles.css
├── posts
│ ├── 03_basic_routing
│ │ ├── get-home.png
│ │ ├── post-home.png
│ │ ├── put-user.png
│ │ └── delete-user.png
│ ├── 14_cors
│ │ └── cors_server_flowchart.png
│ └── 04_simple_json_api
│ │ └── screenshots
│ │ ├── get-api-members.png
│ │ ├── get-api-member-id.png
│ │ ├── post-api-members.png
│ │ ├── put-api-member-id.png
│ │ ├── delete-api-member-id.png
│ │ ├── get-api-member-id-not-found.png
│ │ └── post-api-members-required-fields.png
├── site_libs
│ ├── bootstrap
│ │ └── bootstrap-icons.woff
│ └── quarto-html
│ │ └── tippy.css
└── listings.json
├── 05_router
├── box
│ ├── .Renviron
│ ├── .Rprofile
│ ├── renv
│ │ ├── .gitignore
│ │ └── settings.json
│ ├── data
│ │ ├── members.sqlite
│ │ └── get_db_path.R
│ ├── routes
│ │ └── api
│ │ │ ├── members
│ │ │ ├── get_db_path.R
│ │ │ ├── controllers.R
│ │ │ ├── get_all_members.R
│ │ │ ├── get_member_by_id.R
│ │ │ ├── delete_member.R
│ │ │ ├── create_new_member.R
│ │ │ └── update_member_info.R
│ │ │ └── members.R
│ ├── server.R
│ ├── middleware
│ │ └── logger.R
│ ├── helpers
│ │ ├── get_port.R
│ │ ├── check_port.R
│ │ └── operators.R
│ ├── models
│ │ └── members.R
│ └── README.md
├── r_pkg_structure
│ ├── .Renviron
│ ├── .Rbuildignore
│ ├── .Rprofile
│ ├── server.R
│ ├── renv
│ │ ├── .gitignore
│ │ └── settings.json
│ ├── R
│ │ ├── get_all_members.R
│ │ ├── app.R
│ │ ├── get_port.R
│ │ ├── members.R
│ │ ├── members_router.R
│ │ ├── check_port.R
│ │ ├── get_member_by_id.R
│ │ ├── operators.R
│ │ ├── delete_member.R
│ │ ├── create_new_member.R
│ │ └── update_member_info.R
│ ├── DESCRIPTION
│ └── README.md
└── README.md
├── 09_goals
├── .Rprofile
├── .gitignore
├── renv
│ ├── .gitignore
│ └── settings.json
├── .Renviron
├── models
│ ├── goal_model.R
│ └── user_model.R
├── helpers
│ ├── mongo_query.R
│ ├── get_jwt_secret.R
│ ├── get_port.R
│ ├── generate_token.R
│ ├── to_json.R
│ ├── operators.R
│ ├── truthiness.R
│ └── insert.R
├── config
│ └── db.R
├── server.R
├── routes
│ ├── goal_routes.R
│ └── user_routes.R
├── middleware
│ ├── error_middleware.R
│ └── auth_middleware.R
├── README.md
└── controllers
│ └── goal_controller.R
├── 10_live_reloading
├── .gitignore
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── nodemon.json
├── server.R
├── package.json
└── README.md
├── 14_cors
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── README.md
└── server.R
├── 15_chat
├── .Rprofile
├── ui
│ ├── __init__.R
│ └── page.R
├── renv
│ ├── .gitignore
│ └── settings.json
├── chat
│ ├── __init__.R
│ └── chat.R
├── index.R
└── public
│ ├── style.css
│ ├── ambiorix.js
│ └── chat.js
├── documentation
├── .gitignore
├── styles.css
├── profile.jpg
├── posts
│ ├── 03_basic_routing
│ │ ├── get-home.png
│ │ ├── post-home.png
│ │ ├── put-user.png
│ │ ├── delete-user.png
│ │ └── index.qmd
│ ├── 14_cors
│ │ ├── cors_server_flowchart.png
│ │ └── index.qmd
│ ├── 04_simple_json_api
│ │ └── screenshots
│ │ │ ├── get-api-members.png
│ │ │ ├── post-api-members.png
│ │ │ ├── get-api-member-id.png
│ │ │ ├── put-api-member-id.png
│ │ │ ├── delete-api-member-id.png
│ │ │ ├── get-api-member-id-not-found.png
│ │ │ └── post-api-members-required-fields.png
│ ├── _metadata.yml
│ ├── 00_getting_started
│ │ └── index.qmd
│ ├── 08_datatables
│ │ └── index.qmd
│ ├── 13_parse_raw_json
│ │ └── index.qmd
│ ├── 01_hello_world
│ │ └── index.qmd
│ ├── 12_frontend_for_09_goals
│ │ └── index.qmd
│ ├── 07_dynamic_rendering
│ │ └── index.qmd
│ ├── 06_multi_router
│ │ └── index.qmd
│ ├── 02_static_files
│ │ └── index.qmd
│ └── 05_router
│ │ └── index.qmd
├── index.qmd
├── _quarto.yml
└── about.qmd
├── 08_datatables
├── .Rprofile
├── public
│ └── styles.css
├── renv
│ ├── .gitignore
│ └── settings.json
├── docker-compose.yml
├── utils
│ └── in_prod.R
├── controllers
│ ├── home_get.R
│ └── flights_get.R
├── Dockerfile
├── index.R
├── README.md
└── store
│ ├── create_card.R
│ ├── create_href.R
│ ├── page.R
│ ├── home.R
│ ├── datatable.R
│ └── nav.R
├── 01_hello_world
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── README.md
└── index.R
├── 02_static_files
├── .Rprofile
├── public
│ ├── css
│ │ └── styles.css
│ ├── image.jpg
│ ├── index.html
│ ├── about.html
│ └── index2.R
├── renv
│ ├── .gitignore
│ └── settings.json
├── index.R
└── README.md
├── 03_basic_routing
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── get-home.png
├── post-home.png
├── put-user.png
├── delete-user.png
├── index.R
└── README.md
├── 04_simple_json_api
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
└── screenshots
│ ├── get-api-members.png
│ ├── post-api-members.png
│ ├── get-api-member-id.png
│ ├── put-api-member-id.png
│ ├── delete-api-member-id.png
│ ├── get-api-member-id-not-found.png
│ └── post-api-members-required-fields.png
├── 06_multi_router
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── api
│ ├── members.R
│ ├── v1
│ │ ├── members
│ │ │ ├── controllers.R
│ │ │ ├── delete_member.R
│ │ │ ├── get_all_members.R
│ │ │ ├── create_new_member.R
│ │ │ ├── update_member_info.R
│ │ │ └── get_member_by_id.R
│ │ └── members.R
│ └── v2
│ │ ├── members
│ │ ├── controllers.R
│ │ ├── delete_member.R
│ │ ├── get_all_members.R
│ │ ├── create_new_member.R
│ │ ├── update_member_info.R
│ │ └── get_member_by_id.R
│ │ └── members.R
├── server.R
└── README.md
├── 11_csv_xlsx_upload
├── .Rprofile
├── renv
│ ├── .gitignore
│ └── settings.json
├── iris.xlsx
├── .gitignore
├── README.md
└── example.R
├── 13_parse_raw_json
├── .Rprofile
├── .gitignore
├── renv
│ ├── .gitignore
│ └── settings.json
├── README.md
└── server.R
├── 07_dynamic_rendering
├── .Rprofile
├── public
│ └── styles.css
├── renv
│ ├── .gitignore
│ └── settings.json
├── controllers
│ ├── template_path.R
│ ├── contact.R
│ ├── home_get.R
│ ├── about_get.R
│ ├── contact_get.R
│ ├── validate_message.R
│ ├── validate_email.R
│ └── contact_post.R
├── templates
│ ├── template_path.R
│ ├── partials
│ │ ├── footer.html
│ │ └── header.html
│ └── page.html
├── index.R
├── routers
│ └── contact.R
├── store
│ ├── home.R
│ ├── about.R
│ ├── create_card.R
│ ├── nav.R
│ └── contact.R
└── README.md
├── 12_shiny_frontend_for_09_goals
├── .Rprofile
├── modules
│ ├── mod.R
│ ├── goals
│ │ ├── mod.R
│ │ ├── server.R
│ │ ├── proxy.R
│ │ ├── ui.R
│ │ ├── dashboard_ui.R
│ │ └── account_ui.R
│ ├── auth
│ │ ├── mod.R
│ │ ├── login_ui.R
│ │ ├── ui.R
│ │ ├── server.R
│ │ ├── login_server.R
│ │ ├── signup_server.R
│ │ └── proxy.R
│ ├── ui.R
│ └── server.R
├── renv
│ ├── .gitignore
│ └── settings.json
├── .gitignore
├── helpers
│ ├── mod.R
│ ├── env_vars.R
│ └── operators.R
├── store
│ ├── mod.R
│ ├── center_modal.R
│ ├── card.R
│ ├── toast.R
│ └── inputs.R
├── app.R
├── public
│ └── script.js
└── README.md
└── README.md
/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .lintr
2 |
--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
1 | /* css styles */
2 |
--------------------------------------------------------------------------------
/05_router/box/.Renviron:
--------------------------------------------------------------------------------
1 | PORT = 3000
2 |
--------------------------------------------------------------------------------
/09_goals/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/10_live_reloading/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/14_cors/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/15_chat/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/documentation/.gitignore:
--------------------------------------------------------------------------------
1 | /.quarto/
2 | dev.R
3 |
--------------------------------------------------------------------------------
/documentation/styles.css:
--------------------------------------------------------------------------------
1 | /* css styles */
2 |
--------------------------------------------------------------------------------
/05_router/box/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/.Renviron:
--------------------------------------------------------------------------------
1 | PORT = 3000
2 |
--------------------------------------------------------------------------------
/08_datatables/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/01_hello_world/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/02_static_files/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/03_basic_routing/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/04_simple_json_api/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/06_multi_router/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/10_live_reloading/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/13_parse_raw_json/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^renv$
2 | ^renv\.lock$
3 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/.Rprofile:
--------------------------------------------------------------------------------
1 | source("renv/activate.R")
2 |
--------------------------------------------------------------------------------
/15_chat/ui/__init__.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | ./page[page],
4 | )
5 |
--------------------------------------------------------------------------------
/08_datatables/public/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Poppins", sans-serif;
3 | }
4 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/public/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Poppins", sans-serif;
3 | }
4 |
--------------------------------------------------------------------------------
/02_static_files/public/css/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #333;
3 | color: #fff;
4 | }
5 |
--------------------------------------------------------------------------------
/09_goals/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 | package.json
4 | nodemon.json
5 | dev.R
6 |
--------------------------------------------------------------------------------
/09_goals/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/14_cors/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/15_chat/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/05_router/box/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/08_datatables/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/01_hello_world/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/02_static_files/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/03_basic_routing/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/04_simple_json_api/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/06_multi_router/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/10_live_reloading/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/13_parse_raw_json/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | nodemon.json
3 | package-lock.json
4 | package.json
5 | .Renviron
6 |
--------------------------------------------------------------------------------
/13_parse_raw_json/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/15_chat/chat/__init__.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | ./chat[
4 | chat_get,
5 | chat_ws,
6 | ],
7 | )
8 |
--------------------------------------------------------------------------------
/documentation/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/profile.jpg
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/server.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 |
3 | pkgload::load_all()
4 |
5 | app()$start(open = FALSE)
6 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/10_live_reloading/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "execMap": {
3 | "R": "Rscript"
4 | },
5 | "ext": "R,html,css,js"
6 | }
7 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/iris.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/11_csv_xlsx_upload/iris.xlsx
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / ui[ui],
4 | . / server[server],
5 | )
6 |
--------------------------------------------------------------------------------
/03_basic_routing/get-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/get-home.png
--------------------------------------------------------------------------------
/03_basic_routing/post-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/post-home.png
--------------------------------------------------------------------------------
/03_basic_routing/put-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/put-user.png
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/09_goals/.Renviron:
--------------------------------------------------------------------------------
1 | PORT = 5000
2 | JWT_SECRET = 312d6002-b53a-4045-84ed-7da926f569f9
3 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
4 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | nodemon.json
3 | package-lock.json
4 | package.json
5 | .Renviron
6 | dev.R
7 |
--------------------------------------------------------------------------------
/02_static_files/public/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/02_static_files/public/image.jpg
--------------------------------------------------------------------------------
/03_basic_routing/delete-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/03_basic_routing/delete-user.png
--------------------------------------------------------------------------------
/05_router/box/data/members.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/05_router/box/data/members.sqlite
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/renv/.gitignore:
--------------------------------------------------------------------------------
1 | library/
2 | local/
3 | cellar/
4 | lock/
5 | python/
6 | sandbox/
7 | staging/
8 |
--------------------------------------------------------------------------------
/06_multi_router/api/members.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / v1 / members[v1 = router],
4 | . / v2 / members[v2 = router]
5 | )
6 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/template_path.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | .. / templates / template_path[template_path]
4 | )
5 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | nodemon.json
3 | package-lock.json
4 | package.json
5 | .Renviron
6 | dev.R
7 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/helpers/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / operators[`%||%`],
4 | . / env_vars[get_base_url],
5 | )
6 |
--------------------------------------------------------------------------------
/docs/posts/03_basic_routing/get-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/get-home.png
--------------------------------------------------------------------------------
/docs/posts/03_basic_routing/post-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/post-home.png
--------------------------------------------------------------------------------
/docs/posts/03_basic_routing/put-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/put-user.png
--------------------------------------------------------------------------------
/05_router/box/data/get_db_path.R:
--------------------------------------------------------------------------------
1 | #' Get database path
2 | #' @export
3 | get_db_path <- \() {
4 | file.path(box::file(), "members.sqlite")
5 | }
6 |
--------------------------------------------------------------------------------
/09_goals/models/goal_model.R:
--------------------------------------------------------------------------------
1 | #' Goal schema
2 | #'
3 | goal_schema <- data.frame(
4 | user_id = character(), # oid
5 | text = character()
6 | )
7 |
--------------------------------------------------------------------------------
/docs/posts/03_basic_routing/delete-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/03_basic_routing/delete-user.png
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / ui[goals_ui = ui],
4 | . / server[goals_server = server],
5 | )
6 |
--------------------------------------------------------------------------------
/docs/posts/14_cors/cors_server_flowchart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/14_cors/cors_server_flowchart.png
--------------------------------------------------------------------------------
/docs/site_libs/bootstrap/bootstrap-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/site_libs/bootstrap/bootstrap-icons.woff
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/get_db_path.R:
--------------------------------------------------------------------------------
1 | #' Get path to database
2 | #' @export
3 | box::use(
4 | .. / .. / .. / data / get_db_path[get_db_path]
5 | )
6 |
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/get-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-members.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/post-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/post-api-members.png
--------------------------------------------------------------------------------
/documentation/posts/03_basic_routing/get-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/get-home.png
--------------------------------------------------------------------------------
/documentation/posts/03_basic_routing/post-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/post-home.png
--------------------------------------------------------------------------------
/documentation/posts/03_basic_routing/put-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/put-user.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/get-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-member-id.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/put-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/put-api-member-id.png
--------------------------------------------------------------------------------
/08_datatables/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | datatables:
3 | image: datatables
4 | ports:
5 | - "127.0.0.1:3001:3000"
6 | restart: unless-stopped
7 |
--------------------------------------------------------------------------------
/documentation/posts/03_basic_routing/delete-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/03_basic_routing/delete-user.png
--------------------------------------------------------------------------------
/documentation/posts/14_cors/cors_server_flowchart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/14_cors/cors_server_flowchart.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/delete-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/delete-api-member-id.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/get-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-members.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/get-api-member-id-not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/get-api-member-id-not-found.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/get-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-member-id.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/post-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/post-api-members.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/put-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/put-api-member-id.png
--------------------------------------------------------------------------------
/07_dynamic_rendering/templates/template_path.R:
--------------------------------------------------------------------------------
1 | #' Template path
2 | #'
3 | #' Create a path to `templates/`
4 | #' @export
5 | template_path <- \(...) {
6 | file.path(box::file(), ...)
7 | }
8 |
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/delete-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/delete-api-member-id.png
--------------------------------------------------------------------------------
/04_simple_json_api/screenshots/post-api-members-required-fields.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/04_simple_json_api/screenshots/post-api-members-required-fields.png
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/get-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-members.png
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/post-api-members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/post-api-members.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/get-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-member-id.png
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/put-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/put-api-member-id.png
--------------------------------------------------------------------------------
/08_datatables/utils/in_prod.R:
--------------------------------------------------------------------------------
1 | #' Is app running in prod?
2 | #'
3 | #' @return Logical.
4 | #' @export
5 | in_prod <- \() {
6 | identical(
7 | Sys.getenv("APP_ENV"),
8 | "prod"
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/delete-api-member-id.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/delete-api-member-id.png
--------------------------------------------------------------------------------
/docs/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/docs/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png
--------------------------------------------------------------------------------
/07_dynamic_rendering/templates/partials/footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/get-api-member-id-not-found.png
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/contact.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / contact_get[contact_get],
4 | . / contact_post[contact_post],
5 | . / validate_email[validate_email],
6 | . / validate_message[validate_message]
7 | )
8 |
--------------------------------------------------------------------------------
/documentation/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ambiorix-web/ambiorix-examples/HEAD/documentation/posts/04_simple_json_api/screenshots/post-api-members-required-fields.png
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/get_all_members.R:
--------------------------------------------------------------------------------
1 | #' Get all members controller function
2 | #'
3 | #' @inheritParams handler
4 | #' @name views
5 | #' @keywords internal
6 |
7 | get_all_members <- \(req, res) {
8 | res$json(.GlobalEnv$members)
9 | }
10 |
--------------------------------------------------------------------------------
/10_live_reloading/server.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 |
3 | app <- Ambiorix$new()
4 |
5 | app$get("/", \(req, res){
6 | res$send("Using {ambiorix}!")
7 | })
8 |
9 | app$get("/about", \(req, res){
10 | res$text("About")
11 | })
12 |
13 | app$start()
14 |
--------------------------------------------------------------------------------
/08_datatables/controllers/home_get.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | .. / store / home[home]
3 | )
4 |
5 | #' Home
6 | #'
7 | #' Handler for GET requests at "/". Renders the homepage.
8 | #'
9 | #' @export
10 | home_get <- \(req, res) {
11 | res$send(home())
12 | }
13 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/store/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / inputs[
4 | text_input,
5 | email_input,
6 | password_input,
7 | ],
8 | . / card[card],
9 | . / toast[toast_nofitication],
10 | . / center_modal[center_modal],
11 | )
12 |
--------------------------------------------------------------------------------
/08_datatables/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rocker/r-ver:4.3.3
2 | RUN apt-get update && apt-get install -y \
3 | git-core \
4 | libssl-dev
5 | WORKDIR /app
6 | COPY . .
7 | RUN rm -rdf renv/library
8 | RUN R -e "renv::restore()"
9 | EXPOSE 3000
10 | CMD ["Rscript", "index.R"]
11 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/app.R:
--------------------------------------------------------------------------------
1 | #' Core app
2 | #'
3 | #' @param port Port to run the app in. Defaults to [get_port()].
4 | #' @export
5 | app <- \(port = get_port()) {
6 | app <- ambiorix::Ambiorix$new()
7 | app$listen(port = port)
8 | app$use(members_router())
9 | app
10 | }
11 |
--------------------------------------------------------------------------------
/06_multi_router/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / api / members
4 | )
5 |
6 | Ambiorix$
7 | new()$
8 | listen(port = 3000L)$
9 | use(members$v1)$ # mount API v1 members' router
10 | use(members$v2)$ # mount API v2 members' router
11 | start(open = FALSE)
12 |
--------------------------------------------------------------------------------
/documentation/posts/_metadata.yml:
--------------------------------------------------------------------------------
1 | # options specified here will apply to all posts in this folder
2 |
3 | # freeze computational output
4 | # (see https://quarto.org/docs/projects/code-execution.html#freeze)
5 | freeze: true
6 |
7 | # Enable banner style title blocks
8 | title-block-banner: true
9 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/controllers.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / create_new_member[create_new_member],
4 | . / delete_member[delete_member],
5 | . / get_all_members[get_all_members],
6 | . / get_member_by_id[get_member_by_id],
7 | . / update_member_info[update_member_info]
8 | )
9 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/controllers.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / create_new_member[create_new_member],
4 | . / delete_member[delete_member],
5 | . / get_all_members[get_all_members],
6 | . / get_member_by_id[get_member_by_id],
7 | . / update_member_info[update_member_info]
8 | )
9 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/controllers.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / create_new_member[create_new_member],
4 | . / delete_member[delete_member],
5 | . / get_all_members[get_all_members],
6 | . / get_member_by_id[get_member_by_id],
7 | . / update_member_info[update_member_info]
8 | )
9 |
--------------------------------------------------------------------------------
/09_goals/helpers/mongo_query.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / to_json[to_json]
3 | )
4 |
5 | #' Create a mongo query
6 | #'
7 | #' @param ... `key = value` pairs
8 | #' @examples
9 | #' mongo_query(a = 1, b = 2, c = 3, d = list(e = 5))
10 | #' @export
11 | mongo_query <- \(...) list(...) |> to_json()
12 |
--------------------------------------------------------------------------------
/09_goals/models/user_model.R:
--------------------------------------------------------------------------------
1 | #' User schema
2 | #'
3 | user_schema <- data.frame(
4 | name = character(),
5 | email = character(),
6 | password = character(),
7 | created_at = character(), # timestamp: UTC datetime format
8 | updated_at = character() # timestamp: UTC datetime format
9 | )
10 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/mod.R:
--------------------------------------------------------------------------------
1 | #' @export
2 | box::use(
3 | . / ui[auth_ui = ui],
4 | . / proxy[
5 | login,
6 | delete_account,
7 | req_error_handler,
8 | get_account_details,
9 | update_account_details,
10 | ],
11 | . / server[auth_server = server],
12 | )
13 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/login_ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[NS],
3 | . / signup_ui[create_auth_page],
4 | )
5 |
6 | #' Login UI module
7 | #'
8 | #' @param id String. Module id.
9 | #' @export
10 | ui <- \(id) {
11 | ns <- NS(id)
12 | create_auth_page(ns = ns, type = "login")
13 | }
14 |
--------------------------------------------------------------------------------
/05_router/box/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / helpers / get_port[get_port],
4 | . / middleware / logger[logger],
5 | . / routes / api / members
6 | )
7 |
8 | Ambiorix$
9 | new()$
10 | listen(port = get_port())$
11 | use(logger)$
12 | use(members$router)$
13 | start(open = FALSE)
14 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/templates/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [! partials/header.html !]
6 | [% title %]
7 |
8 |
9 | [% content %]
10 | [! partials/footer.html !]
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/15_chat/chat/chat.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | .. / ui[page, ],
3 | )
4 |
5 | #' @export
6 | chat_get <- function(req, res) {
7 | res$send(page())
8 | }
9 |
10 | #' @export
11 | chat_ws <- function(msg, ws) {
12 | ambiorix::get_websocket_clients() |>
13 | lapply(\(c) {
14 | c$send("chat", msg)
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/15_chat/index.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / chat[
4 | chat_get,
5 | chat_ws,
6 | ],
7 | )
8 |
9 | PORT <- 3000
10 |
11 | app <- Ambiorix$new()
12 |
13 | app$static("public", "static")
14 |
15 | app$get("/", chat_get)
16 | app$receive("chat", chat_ws)
17 |
18 | app$start(port = PORT)
19 |
--------------------------------------------------------------------------------
/08_datatables/index.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / controllers / home_get[home_get],
4 | . / controllers / flights_get[flights_get]
5 | )
6 |
7 | app <- Ambiorix$new(port = 3000L)
8 | app$static("public", "static")
9 | app$get("/", home_get)
10 | app$get("/data/flights", flights_get)
11 | app$start()
12 |
--------------------------------------------------------------------------------
/05_router/box/middleware/logger.R:
--------------------------------------------------------------------------------
1 | #' Logger middleware
2 | #'
3 | #' @export
4 | logger <- \(req, res) {
5 | msg <- paste0(
6 | req$rook.url_scheme,
7 | "://",
8 | req$HTTP_HOST,
9 | req$PATH_INFO,
10 | " ",
11 | format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")
12 | )
13 | cat("\nLogger: ", msg, "\n\n")
14 | }
15 |
--------------------------------------------------------------------------------
/09_goals/config/db.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | mongolite[mongo],
3 | jsonlite[toJSON]
4 | )
5 |
6 | #' Goals db connection
7 | #'
8 | #' @export
9 | goals_conn <- mongo(collection = "goals", db = "mahr_tutorial")
10 |
11 | #' Users db connection
12 | #'
13 | #' @export
14 | users_conn <- mongo(collection = "users", db = "mahr_tutorial")
15 |
--------------------------------------------------------------------------------
/02_static_files/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | My Website
7 |
8 |
9 |
10 | My Website
11 |
12 |
13 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/get_port.R:
--------------------------------------------------------------------------------
1 | #' Get port from .Renviron file
2 | #'
3 | #' Looks for the env var `PORT`
4 | #' @inheritParams cli::cli_abort
5 | #' @return Character vector of length 1
6 | #' @examples
7 | #' \dontrun{
8 | #' get_port()
9 | #' }
10 | get_port <- \(call = rlang::caller_env()) {
11 | check_port(call = call)
12 | Sys.getenv("PORT")
13 | }
14 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/app.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[shinyApp, addResourcePath],
3 | . / modules / mod[ui, server],
4 | )
5 |
6 | addResourcePath(
7 | prefix = "static",
8 | directoryPath = box::file("public")
9 | )
10 |
11 | shinyApp(
12 | ui = ui,
13 | server = server,
14 | options = list(
15 | port = 8000L,
16 | launch.browser = TRUE
17 | )
18 | )
19 |
--------------------------------------------------------------------------------
/15_chat/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | min-height: 100vh;
3 | margin: 0 2rem;
4 | }
5 |
6 | .d-flex {
7 | display: flex;
8 | }
9 |
10 | .d-shrink {
11 | flex-shrink: 1;
12 | }
13 |
14 | .d-grow {
15 | flex-grow: 1;
16 | }
17 |
18 | .mb-1 {
19 | margin-bottom: 0.5rem;
20 | }
21 |
22 | .mr-1 {
23 | margin-right: 0.5rem;
24 | }
25 |
26 | .nes-balloon {
27 | min-width: 80%;
28 | }
29 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/index.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / controllers / home_get[home_get],
4 | . / controllers / about_get[about_get],
5 | . / routers / contact[contact_router = router]
6 | )
7 |
8 | Ambiorix$
9 | new(port = 3000L)$
10 | static("public", "static")$
11 | get("/", home_get)$
12 | get("/about", about_get)$
13 | use(contact_router)$
14 | start(open = TRUE)
15 |
--------------------------------------------------------------------------------
/documentation/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Documentation"
3 | listing:
4 | contents: posts
5 | fields: [title, subtitle, date, author]
6 | sort: "title asc"
7 | type: grid
8 | categories: true
9 | sort-ui: true
10 | filter-ui: false
11 | page-layout: full
12 | title-block-banner: true
13 | ---
14 |
15 | Welcome to the docs for the ✨[ambiorix-examples](https://github.com/ambiorix-web/ambiorix-examples)✨
16 |
--------------------------------------------------------------------------------
/05_router/box/helpers/get_port.R:
--------------------------------------------------------------------------------
1 | box::use(./check_port[check_port])
2 |
3 | #' Get port from .Renviron file
4 | #'
5 | #' Looks for the env var `PORT`
6 | #' @inheritParams cli::cli_abort
7 | #' @return Character vector of length 1
8 | #' @examples
9 | #' \dontrun{
10 | #' get_port()
11 | #' }
12 | #' @export
13 | get_port <- \(call = rlang::caller_env()) {
14 | check_port(call = call)
15 | Sys.getenv("PORT")
16 | }
17 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/home_get.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / template_path[template_path],
3 | .. / store / home[home]
4 | )
5 |
6 | #' Home
7 | #'
8 | #' Handler for GET requests at "/". Renders the homepage.
9 | #'
10 | #' @export
11 | home_get <- \(req, res) {
12 | res$render(
13 | template_path("page.html"),
14 | list(
15 | title = "Home",
16 | content = home()
17 | )
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/delete_member.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Delete member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | delete_member <- \(req, res) {
10 | #
11 | response <- list(message = "Delete member (API v1)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/delete_member.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Delete member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | delete_member <- \(req, res) {
10 | #
11 | response <- list(message = "Delete member (API v2)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/routers/contact.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | .. / controllers / contact[
4 | contact_get,
5 | contact_post,
6 | validate_email,
7 | validate_message
8 | ]
9 | )
10 |
11 | #' @export
12 | router <- Router$new("/contact")
13 |
14 | router$
15 | get("/", contact_get)$
16 | post("/", contact_post)$
17 | post("/email", validate_email)$
18 | post("/message", validate_message)
19 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/about_get.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / template_path[template_path],
3 | .. / store / about[about]
4 | )
5 |
6 | #' About
7 | #'
8 | #' Handler for GET requests at "/about". Renders the about page.
9 | #'
10 | #' @export
11 | about_get <- \(req, res) {
12 | res$render(
13 | template_path("page.html"),
14 | list(
15 | title = "About",
16 | content = about()
17 | )
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/documentation/_quarto.yml:
--------------------------------------------------------------------------------
1 | project:
2 | type: website
3 | output-dir: ../docs
4 |
5 | website:
6 | title: "ambiorix-examples"
7 | navbar:
8 | right:
9 | - about.qmd
10 | - icon: github
11 | href: https://github.com/ambiorix-web/ambiorix-examples
12 | - icon: twitter
13 | href: https://x.com/ambiorixweb
14 | format:
15 | html:
16 | theme: lux
17 | css: styles.css
18 | toc: true
19 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/get_all_members.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Get all members controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | get_all_members <- \(req, res) {
10 | #
11 | response <- list(message = "Get all members (API v1)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/get_all_members.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Get all members controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | get_all_members <- \(req, res) {
10 | #
11 | response <- list(message = "Get all members (API v2)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/09_goals/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | . / helpers / get_port[get_port],
4 | . / middleware / error_middleware[error_handler],
5 | . / routes / goal_routes[goal_router = router],
6 | . / routes / user_routes[user_router = router]
7 | )
8 |
9 | app <- Ambiorix$new()
10 |
11 | app$error <- error_handler
12 |
13 | app$
14 | use(goal_router)$
15 | use(user_router)$
16 | start(port = get_port(), open = FALSE)
17 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/create_new_member.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Create new member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | create_new_member <- \(req, res) {
10 | #
11 | response <- list(message = "Create new member (API v1)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/create_new_member.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Create new member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | create_new_member <- \(req, res) {
10 | #
11 | response <- list(message = "Create new member (API v2)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/update_member_info.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Update member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | update_member_info <- \(req, res) {
10 | #
11 | response <- list(message = "Update member info (API v1)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/update_member_info.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Update member controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | update_member_info <- \(req, res) {
10 | #
11 | response <- list(message = "Update member info (API v2)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/contact_get.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / template_path[template_path],
3 | .. / store / contact[contact]
4 | )
5 |
6 | #' Contact
7 | #'
8 | #' Handler for GET requests at "/contact". Renders the contact page.
9 | #'
10 | #' @export
11 | contact_get <- \(req, res) {
12 | res$render(
13 | template_path("page.html"),
14 | list(
15 | title = "Contact",
16 | content = contact()
17 | )
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/05_router/box/models/members.R:
--------------------------------------------------------------------------------
1 | # box::use(
2 | # RSQLite[SQLite],
3 | # DBI[dbConnect, dbWriteTable, dbListTables]
4 | # )
5 |
6 | # #' The members "model"
7 | # #'
8 | # members <- data.frame(
9 | # id = character(),
10 | # name = character(),
11 | # email = character(),
12 | # status = character()
13 | # )
14 |
15 | # conn <- dbConnect(SQLite(), file.path("data", "members.sqlite"))
16 |
17 | # dbWriteTable(conn = conn, "members", members)
18 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members/get_member_by_id.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Get member by id controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | get_member_by_id <- \(req, res) {
10 | #
11 | response <- list(message = "Get a single member by id (API v1)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members/get_member_by_id.R:
--------------------------------------------------------------------------------
1 | # don't forget to import any deps up here using box::use()
2 |
3 | #' Get member by id controller function
4 | #'
5 | #' @inheritParams handler
6 | #' @name views
7 | #' @keywords internal
8 | #' @export
9 | get_member_by_id <- \(req, res) {
10 | #
11 | response <- list(message = "Get a single member by id (API v2)")
12 | res$set_status(200)$json(response)
13 | }
14 |
--------------------------------------------------------------------------------
/01_hello_world/README.md:
--------------------------------------------------------------------------------
1 | ## Hello World example
2 |
3 | This app starts a server and listens on port 3000 for connections.
4 |
5 | The app responds with "Hello World" for requests to the root
6 | URL (/) or **route**.
7 |
8 | - Navigate to [localhost:3000/](http://localhost:3000/) on your browser to see that.
9 | - Navigate to [localhost:3000/about](http://localhost:3000/about) to see the about page.
10 |
11 | For every other path, it will respond with a **404 Not Found**.
12 |
--------------------------------------------------------------------------------
/03_basic_routing/index.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 | PORT <- 3000
3 |
4 | app <- Ambiorix$new()
5 |
6 | app$get("/", \(req, res) {
7 | res$send("Hello World!")
8 | })
9 |
10 | app$post("/", \(req, res) {
11 | res$send("Got a POST request")
12 | })
13 |
14 | app$put("/user", \(req, res) {
15 | res$send("Got a PUT request at /user")
16 | })
17 |
18 | app$delete("/user", \(req, res) {
19 | res$send("Got a DELETE request at /user")
20 | })
21 |
22 | app$start(port = PORT)
23 |
--------------------------------------------------------------------------------
/09_goals/helpers/get_jwt_secret.R:
--------------------------------------------------------------------------------
1 | #' Get jwt secret key
2 | #'
3 | #' Looks for the variable `JWT_SECRET` in your .Renviron
4 | #' @export
5 | get_jwt_secret <- \() {
6 | key <- Sys.getenv("JWT_SECRET")
7 | if (key == "") {
8 | stop(
9 | "JWT secret key not found.",
10 | " Set the `JWT_SECRET` variable in your .Renviron.",
11 | " You can use `uuid::UUIDgenerate()` to generate a secret key.",
12 | call. = FALSE
13 | )
14 | }
15 | key
16 | }
17 |
--------------------------------------------------------------------------------
/01_hello_world/index.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 |
3 | # port to listen on:
4 | PORT <- 3000
5 |
6 | # initialize a new ambiorix instance:
7 | app <- Ambiorix$new()
8 |
9 | # respond with “Hello World!” for requests to the root URL (/) or route:
10 | app$get("/", \(req, res) {
11 | res$send("Hello World!")
12 | })
13 |
14 | # you can also use html tags:
15 | app$get("/about", \(req, res) {
16 | res$send("About Us
")
17 | })
18 |
19 | # start server:
20 | app$start(port = PORT)
21 |
--------------------------------------------------------------------------------
/09_goals/routes/goal_routes.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | .. / controllers / goal_controller[
4 | get_goals,
5 | set_goal,
6 | update_goal,
7 | delete_goal
8 | ],
9 | .. / middleware / auth_middleware[protect]
10 | )
11 |
12 | #' Goal router
13 | #'
14 | #' @export
15 | router <- Router$
16 | new("/api/goals")$
17 | use(protect)$
18 | get("/", get_goals)$
19 | post("/", set_goal)$
20 | put("/:id", update_goal)$
21 | delete("/:id", delete_goal)
22 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/store/center_modal.R:
--------------------------------------------------------------------------------
1 | #' Center modal dialog
2 | #'
3 | #' Vertically and horizontally centers [shiny::modalDialog]
4 | #'
5 | #' @param modal [shiny::modalDialog]
6 | #' @inherit email_verification_modal examples
7 | #' @return The centered modal dialog tags
8 | #' @export
9 | center_modal <- \(modal) {
10 | modal_tag_q <- modal |> htmltools::tagQuery()
11 | modal_tag_q$find(".modal-dialog")$addClass("modal-dialog-centered")
12 | modal_tag_q$allTags()
13 | }
14 |
--------------------------------------------------------------------------------
/02_static_files/public/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | About
7 |
8 |
9 |
10 | About Us
11 |
12 | - We're concerned about you
13 | - We are a global team
14 | - Integrity is at the core of our values
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/09_goals/middleware/error_middleware.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | cli[cli_alert_danger],
3 | .. / helpers / operators[`%||%`]
4 | )
5 |
6 | #' Error handler
7 | #'
8 | #' @export
9 | error_handler <- \(req, res, error = NULL) {
10 | if (!is.null(error)) {
11 | error_msg <- conditionMessage(error)
12 | cli_alert_danger("Error: {error_msg}")
13 | }
14 | response <- list(
15 | code = 500L,
16 | msg = "A server error occurred!"
17 | )
18 |
19 | res$set_status(500L)$json(response)
20 | }
21 |
--------------------------------------------------------------------------------
/09_goals/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/14_cors/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/15_chat/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/01_hello_world/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/02_static_files/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/03_basic_routing/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/05_router/box/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/06_multi_router/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/08_datatables/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/09_goals/helpers/get_port.R:
--------------------------------------------------------------------------------
1 | #' Get port to run app on from .Renviron file
2 | #'
3 | #' Looks for the env var `PORT`, if not found defaults
4 | #' to `default`.
5 | #' @param default Default value for the port in case none is set in
6 | #' `.Renviron`
7 | #' @return Character vector of length 1
8 | #' @examples
9 | #' \dontrun{
10 | #' get_port()
11 | #' }
12 | #' @export
13 | get_port <- \(default = 8000) {
14 | port <- Sys.getenv("PORT")
15 | port <- if (port == "") default else port
16 | as.integer(port)
17 | }
18 |
--------------------------------------------------------------------------------
/04_simple_json_api/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/10_live_reloading/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/store/card.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags],
3 | )
4 |
5 | #' Create a bootstrap card
6 | #'
7 | #' @param ... [htmltools::tags] Card content.
8 | #' @param class Character vector. Classes to apply to the card body.
9 | #' @return [htlmtools::tags$div()]
10 | #' @export
11 | card <- \(..., class = NULL) {
12 | class <- c("card-body", class)
13 |
14 | tags$div(
15 | class = "card border-2 border-light",
16 | tags$div(
17 | class = class,
18 | ...
19 | )
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/13_parse_raw_json/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | . / members / controllers[...]
4 | )
5 |
6 | #' @export
7 | router <- Router$new("/api/members")
8 |
9 | # get all members:
10 | router$get("/", get_all_members)
11 |
12 | # get a single member:
13 | router$get("/:id", get_member_by_id)
14 |
15 | # create a new member:
16 | router$post("/", create_new_member)
17 |
18 | # update member info:
19 | router$put("/:id", update_member_info)
20 |
21 | # delete member:
22 | router$delete("/:id", delete_member)
23 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/06_multi_router/api/v1/members.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | . / members / controllers[...]
4 | )
5 |
6 | #' @export
7 | router <- Router$new("/api/v1/members")
8 |
9 | # get all members:
10 | router$get("/", get_all_members)
11 |
12 | # get a single member:
13 | router$get("/:id", get_member_by_id)
14 |
15 | # create a new member:
16 | router$post("/", create_new_member)
17 |
18 | # update member info:
19 | router$put("/:id", update_member_info)
20 |
21 | # delete member:
22 | router$delete("/:id", delete_member)
23 |
--------------------------------------------------------------------------------
/06_multi_router/api/v2/members.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | . / members / controllers[...]
4 | )
5 |
6 | #' @export
7 | router <- Router$new("/api/v2/members")
8 |
9 | # get all members:
10 | router$get("/", get_all_members)
11 |
12 | # get a single member:
13 | router$get("/:id", get_member_by_id)
14 |
15 | # create a new member:
16 | router$post("/", create_new_member)
17 |
18 | # update member info:
19 | router$put("/:id", update_member_info)
20 |
21 | # delete member:
22 | router$delete("/:id", delete_member)
23 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/renv/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "bioconductor.version": null,
3 | "external.libraries": [],
4 | "ignored.packages": [],
5 | "package.dependency.fields": [
6 | "Imports",
7 | "Depends",
8 | "LinkingTo"
9 | ],
10 | "ppm.enabled": null,
11 | "ppm.ignored.urls": [],
12 | "r.version": null,
13 | "snapshot.type": "implicit",
14 | "use.cache": true,
15 | "vcs.ignore.cellar": true,
16 | "vcs.ignore.library": true,
17 | "vcs.ignore.local": true,
18 | "vcs.manage.ignores": true
19 | }
20 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/get_all_members.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / get_db_path[get_db_path],
3 | DBI[dbConnect, dbDisconnect, dbReadTable],
4 | RSQLite[SQLite]
5 | )
6 |
7 | #' Get all members controller function
8 | #'
9 | #' @inheritParams handler
10 | #' @name views
11 | #' @keywords internal
12 | #' @export
13 | get_all_members <- \(req, res) {
14 | conn <- dbConnect(drv = SQLite(), get_db_path())
15 | on.exit(dbDisconnect(conn))
16 |
17 | members <- dbReadTable(conn = conn, name = "members")
18 | res$json(members)
19 | }
20 |
--------------------------------------------------------------------------------
/09_goals/routes/user_routes.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Router],
3 | .. / controllers / user_controller[
4 | register_user,
5 | login_user,
6 | update_me,
7 | delete_me,
8 | get_me,
9 | ],
10 | .. / middleware / auth_middleware[protect]
11 | )
12 |
13 | #' Users router
14 | #'
15 | #' @export
16 | router <- Router$
17 | new("/api/users")$
18 | use(protect)$
19 | post("/", register_user)$
20 | post("/login", login_user)$
21 | delete("/me", delete_me)$
22 | put("/me", update_me)$
23 | get("/me", get_me)
24 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/members.R:
--------------------------------------------------------------------------------
1 | #' The members data.frame
2 | #'
3 | #' This is a global variable. This is NOT a good practice. It is here for
4 | #' example purposes ie. to provide a complete reprex.
5 | #' Please NEVER do this. You should use a database instead.
6 | members <- data.frame(
7 | id = as.character(1:3),
8 | name = c("John Doe", "Bob Williams", "Shannon Jackson"),
9 | email = c("john@gmail.com", "bob@gmail.com", "shannon@gmail.com"),
10 | status = c("active", "inactive", "active")
11 | )
12 |
13 | .GlobalEnv$members <- members
14 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/members_router.R:
--------------------------------------------------------------------------------
1 | #' Members router
2 | #'
3 | #' @export
4 | members_router <- \() {
5 | router <- Router$new("/api/members")
6 |
7 | # get all members:
8 | router$get("/", get_all_members)
9 |
10 | # get a single member:
11 | router$get("/:id", get_member_by_id)
12 |
13 | # create a new member:
14 | router$post("/", create_new_member)
15 |
16 | # update member info:
17 | router$put("/:id", update_member_info)
18 |
19 | # delete member:
20 | router$delete("/:id", delete_member)
21 |
22 | router
23 | }
24 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/README.md:
--------------------------------------------------------------------------------
1 | # File upload
2 |
3 | This example shows how you can upload & parse csv and xlsx files in an ambiorix
4 | API.
5 |
6 | ## Run API
7 |
8 | 1. `cd` into the `11_csv_xlsx_upload/` dir:
9 |
10 | ```bash
11 | cd 11_csv_xlsx_upload/
12 | ```
13 |
14 | 1. Fire up R and restore package dependencies:
15 |
16 | ```r
17 | renv::restore()
18 | ```
19 |
20 | 1. `server.R` is the entry point. Run this command in the terminal to start the
21 | API:
22 |
23 | ```bash
24 | Rscript server.R
25 | ```
26 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/public/script.js:
--------------------------------------------------------------------------------
1 | function toggle_password(password_input_id, toggle_btn_id) {
2 | $(document).ready(function () {
3 | $("#" + toggle_btn_id).on("click", function () {
4 | let password_input = $("#" + password_input_id);
5 | let password_field_type = password_input.attr("type");
6 |
7 | password_input.attr(
8 | "type",
9 | password_field_type === "password" ? "text" : "password"
10 | );
11 |
12 | $(this).find("i").toggleClass("fa-eye fa-eye-slash");
13 | });
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/templates/partials/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/14_cors/README.md:
--------------------------------------------------------------------------------
1 | # CORS middleware
2 |
3 | This example shows how you can add a CORS middleware to your API.
4 |
5 | This is useful when making cross-origin requests from the browser (client-side).
6 |
7 | # Run API
8 |
9 | 1. `cd` into the `14_cors/` dir:
10 |
11 | ```bash
12 | cd 14_cors/
13 | ```
14 | 1. Fire up R and restore package dependencies:
15 |
16 | ```r
17 | renv::restore()
18 | ```
19 | 1. `server.R` is the entry point. Run this command in the terminal to start the API:
20 |
21 | ```bash
22 | Rscript server.R
23 | ```
24 |
--------------------------------------------------------------------------------
/10_live_reloading/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "10_live_reloading",
3 | "version": "1.0.0",
4 | "description": "When building applications (whether backend or frontend), it gets tiring to manually stop & restart the app when you make changes to the source code.",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon --signal SIGTERM server.R"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "nodemon": "^3.0.3"
14 | },
15 | "dependencies": {
16 | "braces": "^3.0.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/09_goals/helpers/generate_token.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | jose[jwt_claim, jwt_encode_hmac],
3 | lubridate[now],
4 | . / get_jwt_secret[get_jwt_secret]
5 | )
6 |
7 | #' Generate JWT
8 | #'
9 | #' @param user_id User id
10 | #' @param expires_in How long before the token expires. Defaults to 1 month.
11 | #' @export
12 | generate_token <- \(
13 | user_id,
14 | expires_in = now("UTC") + months(1, abbreviate = FALSE)
15 | ) {
16 | jwt_encode_hmac(
17 | claim = jwt_claim(
18 | exp = expires_in,
19 | user_id = user_id
20 | ),
21 | secret = get_jwt_secret()
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/02_static_files/public/index2.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 | library(htmltools)
3 | PORT <- 3000
4 |
5 | app <- Ambiorix$new()
6 |
7 | app$static(path = "public", "static")
8 |
9 | app$get("/", \(req, res) {
10 | res$send(
11 | tags$div(
12 | tags$h1("Hello everyone"),
13 | tags$img(src = file.path("static", "image.jpg"))
14 | )
15 | )
16 | })
17 |
18 | app$start(port = PORT)
19 |
20 | # run the app and navigate to these links in your browser:
21 | # - http://localhost:3000/static/index.html
22 | # - http://localhost:3000/static/about.html
23 | # - http://localhost:3000/static/index2.R
24 |
--------------------------------------------------------------------------------
/documentation/posts/00_getting_started/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "00: getting started"
3 | subtitle: "Installation"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [00_getting_started]
7 | ---
8 |
9 | ## Installation
10 |
11 | - Clone the repo:
12 |
13 | ```bash
14 | git clone git@github.com:ambiorix-web/ambiorix-examples.git
15 | ```
16 |
17 | - `cd` into the root dir:
18 |
19 | ```bash
20 | cd ambiorix-examples
21 | ```
22 |
23 | All the examples assume you're in this directory.
24 |
25 | ## Say hello
26 |
27 | You're now ready for a ✨[Hello, World!](../01_hello_world/index.qmd)✨
28 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: members
2 | Title: Create Members REST API
3 | Version: 0.0.0.9000
4 | Authors@R:
5 | person("your-first-name", "your-last-name", , "example@gmail.com", role = c("aut", "cre", "cph"),
6 | comment = c(ORCID = "your-orc-id"))
7 | Description: Generate a RESTful API using ambiorix via the standard R package structure. This is just an example.
8 | Encoding: UTF-8
9 | Roxygen: list(markdown = TRUE)
10 | RoxygenNote: 7.2.3
11 | Imports:
12 | ambiorix (>= 2.1.0),
13 | htmltools (>= 0.5.7),
14 | cli (>= 3.6.2),
15 | rlang (>= 1.1.2)
16 | Suggests:
17 | ambiorix.generator (>= 1.0.2)
18 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/store/home.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / nav[nav],
4 | . / create_card[create_card]
5 | )
6 |
7 | #' The "Home" page
8 | #'
9 | #' @return An object of class `shiny.tag`
10 | #' @export
11 | home <- \() {
12 | tagList(
13 | nav(),
14 | tags$div(
15 | class = "container",
16 | create_card(
17 | title = "Hello World!",
18 | title_icon = tags$i(class = "bi bi-house-door"),
19 | title_class = "text-primary",
20 | class = "shadow-sm",
21 | tags$p("This is our homepage."),
22 | tags$p("ambiorix + htmx = 🚀")
23 | )
24 | )
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/check_port.R:
--------------------------------------------------------------------------------
1 | #' Checks if the env var `PORT` is set
2 | #'
3 | #' @inheritParams cli::cli_abort
4 | #' @return `TRUE` (invisibly) if the env var `PORT` is found in `.Renviron`.
5 | #' Otherwise, throws an error.
6 | #' @examples
7 | #' \dontrun{
8 | #' check_port()
9 | #' }
10 | check_port <- \(call = rlang::caller_env()) {
11 | if (Sys.getenv("PORT") == "") {
12 | cli::cli_abort(
13 | message = c(
14 | "x" = "Env var {.envvar PORT} not found.",
15 | "i" = "Please set it in your {.file .Renviron} and restart the R session."
16 | ),
17 | call = call
18 | )
19 | }
20 | invisible(TRUE)
21 | }
22 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | NS,
4 | tabsetPanel,
5 | tabPanelBody,
6 | ],
7 | htmltools[tags],
8 | . / login_ui[login_ui = ui],
9 | . / signup_ui[signup_ui = ui],
10 | )
11 |
12 | #' Auth module UI
13 | #'
14 | #' @param id String. Module id.
15 | #' @export
16 | ui <- \(id) {
17 | ns <- NS(id)
18 |
19 | tabsetPanel(
20 | id = ns("tabs"),
21 | type = "hidden",
22 | selected = "login",
23 | tabPanelBody(
24 | value = "login",
25 | login_ui(id = ns("login"))
26 | ),
27 | tabPanelBody(
28 | value = "signup",
29 | signup_ui(id = ns("signup"))
30 | )
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/05_router/box/helpers/check_port.R:
--------------------------------------------------------------------------------
1 | box::use(cli[cli_abort])
2 | #' Checks if the env var `PORT` is set
3 | #'
4 | #' @inheritParams cli::cli_abort
5 | #' @return `TRUE` (invisibly) if the env var `PORT` is found in `.Renviron`.
6 | #' Otherwise, throws an error.
7 | #' @examples
8 | #' \dontrun{
9 | #' check_port()
10 | #' }
11 | #' @export
12 | check_port <- \(call = rlang::caller_env()) {
13 | if (Sys.getenv("PORT") == "") {
14 | cli_abort(
15 | message = c(
16 | "x" = "Env var {.envvar PORT} not found.",
17 | "i" = "Please set it in your {.file .Renviron} and restart the R session."
18 | ),
19 | call = call
20 | )
21 | }
22 | invisible(TRUE)
23 | }
24 |
--------------------------------------------------------------------------------
/02_static_files/index.R:
--------------------------------------------------------------------------------
1 | library(ambiorix)
2 | library(htmltools)
3 | PORT <- 3000
4 |
5 | app <- Ambiorix$new()
6 |
7 | # make the "public/" folder static, and accessible as "static/":
8 | app$static(path = "public", "static")
9 | # any files/folders in "public/" can now be accessed via your browser
10 |
11 | app$get("/", \(req, res) {
12 | res$send(
13 | tags$div(
14 | tags$h1("Hello everyone"),
15 | tags$img(src = file.path("static", "image.jpg"))
16 | )
17 | )
18 | })
19 |
20 | app$start(port = PORT)
21 |
22 | # run the app and navigate to these links in your browser:
23 | # - http://localhost:3000/static/index.html
24 | # - http://localhost:3000/static/about.html
25 | # - http://localhost:3000/static/index2.R
26 |
--------------------------------------------------------------------------------
/09_goals/helpers/to_json.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | jsonlite[toJSON]
3 | )
4 |
5 | #' Convert R objects to JSON with some custom defaults
6 | #'
7 | #' @inheritParams jsonlite::toJSON
8 | #' @examples
9 | #' query <- list(
10 | #' name = "Larry",
11 | #' age = 32,
12 | #' gpa = 2.8,
13 | #' fullTime = FALSE,
14 | #' registerDate = lubridate::now("UTC"),
15 | #' courses = c("Biology", "Chemistry", "Calculus"),
16 | #' address = list(
17 | #' street = "123 Fake St.",
18 | #' city = "Bikini Bottom",
19 | #' zip = 12345
20 | #' )
21 | #' ) |>
22 | #' to_json()
23 | #'
24 | #' query
25 | #' @export
26 | to_json <- \(x, pretty = TRUE, auto_unbox = TRUE, ...) {
27 | toJSON(x = x, pretty = TRUE, auto_unbox = TRUE, ...)
28 | }
29 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/get_member_by_id.R:
--------------------------------------------------------------------------------
1 | #' Get member by id controller function
2 | #'
3 | #' @inheritParams handler
4 | #' @name views
5 | #' @keywords internal
6 | get_member_by_id <- \(req, res) {
7 | # get the supplied id:
8 | member_id <- req$params$id
9 |
10 | members <- .GlobalEnv$members
11 | # filter member with that id:
12 | found <- members |> dplyr::filter(id == member_id)
13 |
14 | # if a member with that id was found, return the member:
15 | if (nrow(found) > 0) {
16 | return(res$json(found))
17 | }
18 |
19 | # otherwise, change response status to 400 (Bad Request)
20 | # and provide a message:
21 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
22 | res$set_status(400L)$json(msg)
23 | }
24 |
--------------------------------------------------------------------------------
/09_goals/helpers/operators.R:
--------------------------------------------------------------------------------
1 | #' Coalescing operator to specify a default value
2 | #'
3 | #' This operator is used to specify a default value for a variable if the
4 | #' original value is \code{NULL}.
5 | #'
6 | #' @param x a variable to check for \code{NULL}
7 | #' @param y the default value to return if \code{x} is \code{NULL}
8 | #' @return the first non-\code{NULL} value
9 | #' @name op-null-default
10 | #' @examples
11 | #' my_var <- NULL
12 | #' default_value <- "hello"
13 | #' result <- my_var %||% default_value
14 | #' result # "hello"
15 | #'
16 | #' my_var <- "world"
17 | #' default_value <- "hello"
18 | #' result <- my_var %||% default_value
19 | #' result # "world"
20 | #'
21 | #' @export
22 | "%||%" <- \(x, y) {
23 | if (is.null(x)) y else x
24 | }
25 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/helpers/env_vars.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | cli[cli_abort],
3 | rlang[caller_env],
4 | )
5 |
6 | #' Get the backend's base url
7 | #'
8 | #' Checks for the env var `BASE_URL` in your .Renviron
9 | #' @param call [rlang::caller_env()] Environment of the calling function.
10 | #' @return String.
11 | #' @export
12 | get_base_url <- \(call = caller_env()) {
13 | base_url <- Sys.getenv("BASE_URL")
14 |
15 | not_found <- identical(base_url, "")
16 | if (not_found) {
17 | cli_abort(
18 | message = c(
19 | "x" = "env var {.envvar BASE_URL} not found in the project's {.file .Renviron}",
20 | "i" = "Did you forget to set it and restart your R session?"
21 | )
22 | )
23 | }
24 |
25 | base_url
26 | }
27 |
--------------------------------------------------------------------------------
/05_router/box/helpers/operators.R:
--------------------------------------------------------------------------------
1 | #' Coalescing operator to specify a default value
2 | #'
3 | #' This operator is used to specify a default value for a variable if the
4 | #' original value is \code{NULL}.
5 | #'
6 | #' @param x a variable to check for \code{NULL}
7 | #' @param y the default value to return if \code{x} is \code{NULL}
8 | #' @return the first non-\code{NULL} value
9 | #' @name op-null-default
10 | #' @export
11 | #' @examples
12 | #' my_var <- NULL
13 | #' default_value <- "hello"
14 | #' result <- my_var %||% default_value
15 | #' result # "hello"
16 | #'
17 | #' my_var <- "world"
18 | #' default_value <- "hello"
19 | #' result <- my_var %||% default_value
20 | #' result # "world"
21 | #'
22 | "%||%" <- function(x, y) {
23 | if (is.null(x)) y else x
24 | }
25 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/operators.R:
--------------------------------------------------------------------------------
1 | #' Coalescing operator to specify a default value
2 | #'
3 | #' This operator is used to specify a default value for a variable if the
4 | #' original value is \code{NULL}.
5 | #'
6 | #' @param x a variable to check for \code{NULL}
7 | #' @param y the default value to return if \code{x} is \code{NULL}
8 | #' @return the first non-\code{NULL} value
9 | #' @name op-null-default
10 | #' @export
11 | #' @examples
12 | #' my_var <- NULL
13 | #' default_value <- "hello"
14 | #' result <- my_var %||% default_value
15 | #' result # "hello"
16 | #'
17 | #' my_var <- "world"
18 | #' default_value <- "hello"
19 | #' result <- my_var %||% default_value
20 | #' result # "world"
21 | #'
22 | "%||%" <- function(x, y) {
23 | if (is.null(x)) y else x
24 | }
25 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/helpers/operators.R:
--------------------------------------------------------------------------------
1 | #' Coalescing operator to specify a default value
2 | #'
3 | #' This operator is used to specify a default value for a variable if the
4 | #' original value is \code{NULL}.
5 | #'
6 | #' @param x a variable to check for \code{NULL}
7 | #' @param y the default value to return if \code{x} is \code{NULL}
8 | #' @return the first non-\code{NULL} value
9 | #' @name op-null-default
10 | #' @examples
11 | #' my_var <- NULL
12 | #' default_value <- "hello"
13 | #' result <- my_var %||% default_value
14 | #' result # "hello"
15 | #'
16 | #' my_var <- "world"
17 | #' default_value <- "hello"
18 | #' result <- my_var %||% default_value
19 | #' result # "world"
20 | #'
21 | #' @export
22 | "%||%" <- \(x, y) {
23 | if (is.null(x)) y else x
24 | }
25 |
--------------------------------------------------------------------------------
/11_csv_xlsx_upload/example.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | curl[form_file],
3 | httr2[
4 | request,
5 | req_perform,
6 | req_url_path,
7 | last_response,
8 | resp_body_json,
9 | req_body_multipart,
10 | ]
11 | )
12 |
13 | base_url <- "http://127.0.0.1:3000"
14 | file <- form_file(
15 | path = "iris.csv",
16 | type = "text/csv",
17 | name = "iris.csv"
18 | )
19 |
20 | req <- request(base_url = base_url) |>
21 | req_url_path("/csv") |>
22 | req_body_multipart(file = file)
23 |
24 | # use `tryCatch()` in case an error occurs while performing the request:
25 | tryCatch(
26 | expr = req |>
27 | req_perform() |>
28 | resp_body_json(),
29 | error = \(e) {
30 | print("An error occurred!")
31 | error <- last_response() |> resp_body_json()
32 | print(error)
33 | }
34 | )
35 |
--------------------------------------------------------------------------------
/docs/listings.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "listing": "/index.html",
4 | "items": [
5 | "/posts/00_getting_started/index.html",
6 | "/posts/01_hello_world/index.html",
7 | "/posts/02_static_files/index.html",
8 | "/posts/03_basic_routing/index.html",
9 | "/posts/04_simple_json_api/index.html",
10 | "/posts/05_router/index.html",
11 | "/posts/06_multi_router/index.html",
12 | "/posts/07_dynamic_rendering/index.html",
13 | "/posts/08_datatables/index.html",
14 | "/posts/09_goals/index.html",
15 | "/posts/10_live_reloading/index.html",
16 | "/posts/11_csv_xlsx_upload/index.html",
17 | "/posts/12_frontend_for_09_goals/index.html",
18 | "/posts/13_parse_raw_json/index.html",
19 | "/posts/14_cors/index.html"
20 | ]
21 | }
22 | ]
--------------------------------------------------------------------------------
/09_goals/helpers/truthiness.R:
--------------------------------------------------------------------------------
1 | #' Is a value falsy?
2 | #'
3 | #' @description
4 | #' A falsy value is either:
5 | #' * `NULL`
6 | #' * An empty string ""
7 | #' * Has a length of 0
8 | #' A truthy value is the opposite of that.
9 | #' Please note that this is not a check for whether a value is `TRUE` or
10 | #' `FALSE`.
11 | #' @param x Value to check.
12 | #' @examples
13 | #' is_falsy("") # TRUE
14 | #' is_falsy("m") # FALSE
15 | #' is_falsy(NULL) # TRUE
16 | #' is_falsy(character(0L)) # TRUE
17 | #'
18 | #' is_truthy("") # FALSE
19 | #' is_truthy("m") # TRUE
20 | #' is_truthy(NULL) # FALSE
21 | #' is_truthy(character(0L)) # FALSE
22 | #' @return Logical.
23 | #' @export
24 | is_falsy <- \(x) {
25 | is.null(x) || identical(x, "") || length(x) == 0L
26 | }
27 |
28 | #' Is a value truthy?
29 | #'
30 | #' @rdname is_falsy
31 | #' @export
32 | is_truthy <- Negate(is_falsy)
33 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/store/about.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / nav[nav],
4 | . / create_card[create_card]
5 | )
6 |
7 | #' The "About" page
8 | #'
9 | #' @return An object of class `shiny.tag`
10 | #' @export
11 | about <- \() {
12 | tagList(
13 | nav(active = "About"),
14 | tags$div(
15 | class = "container",
16 | create_card(
17 | title = "About Us",
18 | title_icon = tags$i(class = "bi bi-info-circle"),
19 | title_class = "text-primary",
20 | class = "shadow-sm",
21 | tags$p("Here are a few things you might want to know:"),
22 | tags$ul(
23 | tags$li("Ambiorix is an unopinionated framework"),
24 | tags$li("It doesn't care about what you decide to use for your frontend"),
25 | tags$li("Ambiorix works great with htmx, btw.")
26 | )
27 | )
28 | )
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags],
3 | bslib[page, bs_theme],
4 | shinytoastr[useToastr],
5 | cookies[add_cookie_handlers],
6 | shiny[tabsetPanel, tabPanelBody],
7 | shinyjs[useShinyjs],
8 | . / auth / mod[auth_ui],
9 | . / goals / mod[goals_ui],
10 | )
11 |
12 | #' App UI
13 | #'
14 | #' @export
15 | ui <- page(
16 | title = "Goals",
17 | theme = bs_theme(version = 5, preset = "zephyr"),
18 | lang = "en",
19 | tags$head(
20 | tags$script(src = "static/script.js")
21 | ),
22 | useShinyjs(),
23 | useToastr(),
24 | tabsetPanel(
25 | id = "pages",
26 | type = "hidden",
27 | selected = "auth",
28 | tabPanelBody(
29 | value = "auth",
30 | auth_ui(id = "auth")
31 | ),
32 | tabPanelBody(
33 | value = "goals",
34 | goals_ui(id = "goals")
35 | )
36 | )
37 | ) |>
38 | add_cookie_handlers()
39 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/README.md:
--------------------------------------------------------------------------------
1 | ## Using standard R package structure with ambiorix
2 |
3 | Run the file [index.R](./index.R) to fire up the app.
4 |
5 | > [!CAUTION]
6 | > - In this example, I have commited the [.Renviron](./.Renviron) file. **NEVER** do that! This is just for example purposes and to kind of create a full reprex.
7 | > - I have also used and manipulated a global env var (`.GlobalEnv$members`). Please **NEVER** do this. You should use a database instead.
8 |
9 | Once you run the app, you'll be able to send requests via these endpoints:
10 | - `GET`: `http://localhost:3000/api/members` : Gets all members
11 | - `GET`: `http://localhost:3000/api/members/:id` : Get a single member by id
12 | - `POST`: `http://localhost:3000/api/members` : Create a new member
13 | - `PUT`: `http://localhost:3000/api/members/:id` : Update member info
14 | - `DELETE`: `http://localhost:3000/api/members/:id` : Delete a member
15 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/delete_member.R:
--------------------------------------------------------------------------------
1 | #' Delete member controller function
2 | #'
3 | #' @inheritParams handler
4 | #' @name views
5 | #' @keywords internal
6 | delete_member <- \(req, res) {
7 | # get the supplied id:
8 | member_id <- req$params$id
9 |
10 | members <- .GlobalEnv$members
11 | # filter member with that id:
12 | found <- members |> dplyr::filter(id == member_id)
13 |
14 | # if a member with that id is NOT found, change response status
15 | # and provide a message:
16 | if (nrow(found) == 0) {
17 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
18 | return(res$set_status(400L)$json(msg))
19 | }
20 |
21 | # otherwise, proceed to delete member:
22 | .GlobalEnv$members <- members |> dplyr::filter(id != member_id)
23 |
24 | response <- list(
25 | msg = "Member deleted successfully",
26 | members = .GlobalEnv$members
27 | )
28 | res$json(response)
29 | }
30 |
--------------------------------------------------------------------------------
/13_parse_raw_json/README.md:
--------------------------------------------------------------------------------
1 | # Parse raw JSON
2 |
3 | Say you have an API endpoint that expects raw JSON data. How do you parse that?
4 |
5 | This example answers that and is in a way related to the example on [csv & xlsx upload](../11_csv_xlsx_upload).
6 |
7 | This is a simple example revolving around how you can select columns and filter
8 | rows in the `iris` dataset when the request body is something like this:
9 |
10 | ```json
11 | {
12 | "cols": ["Sepal.Length", "Petal.Width", "Species"],
13 | "species": ["virginica", "setosa"]
14 | }
15 | ```
16 |
17 | # Run API
18 |
19 | 1. `cd` into the `13_parse_raw_json/` dir:
20 |
21 | ```bash
22 | cd 13_parse_raw_json/
23 | ```
24 | 1. Fire up R and restore package dependencies:
25 |
26 | ```r
27 | renv::restore()
28 | ```
29 | 1. `server.R` is the entry point. Run this command in the terminal to start the API:
30 |
31 | ```bash
32 | Rscript server.R
33 | ```
34 |
--------------------------------------------------------------------------------
/documentation/posts/08_datatables/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "08: datatables"
3 | subtitle: "R users love tables!"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [08_datatables]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `08_datatables/` dir:
12 |
13 | ```bash
14 | cd 08_datatables/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `index.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | At this point, I'm not sure there's anything much to say :)
41 |
42 | ## Goals
43 |
44 | Alright, time to build a ✨[CRUD application backend](../09_goals/index.qmd)✨.
45 |
--------------------------------------------------------------------------------
/08_datatables/README.md:
--------------------------------------------------------------------------------
1 | ## datatables
2 |
3 | how to use [datatables](https://datatables.net/) in ambiorix:
4 |
5 | - rendering
6 | - pagination
7 |
8 | ## deployment
9 |
10 | ### `.Renviron`
11 |
12 | set these env vars when deploying:
13 |
14 | ```
15 | APP_ENV = prod
16 | APP_BASE_PATH = /datatables
17 | ```
18 |
19 | setting the `APP_BASE_PATH` variable is only important if you're deploying
20 | the app at a sub-path.
21 |
22 | for example, if the app is deployed at `https://try.ambiorix.dev/datatables`,
23 | the env var `APP_BASE_PATH` should be set to `/datatables`.
24 |
25 | ### Docker
26 |
27 | - build docker image:
28 |
29 | ```
30 | sudo docker build -t datatables .
31 | ```
32 |
33 | - start services:
34 |
35 | ```
36 | docker compose up -d --remove-orphans
37 | ```
38 |
39 | The app is now accessible on the host machine at **port 3001**.
40 |
41 | - stop services in this context:
42 |
43 | ```
44 | sudo docker compose down
45 | ```
46 |
--------------------------------------------------------------------------------
/08_datatables/store/create_card.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags]
3 | )
4 |
5 | #' Create a custom bootstrap card
6 | #'
7 | #' @param title Card title
8 | #' @param title_icon Icon to place before the title. Can be a `shiny::tags$i()`.
9 | #' @param title_class String. Additional bootstrap classes for the title.
10 | #' @param class Additional classes to apply to the card container
11 | #' @param ... Card content
12 | #' @return [shiny::tags$div]
13 | #' @export
14 | create_card <- \(
15 | ...,
16 | title = NULL,
17 | title_icon = NULL,
18 | title_class = NULL,
19 | class = NULL
20 | ) {
21 | tags$div(
22 | class = paste("card p-1 p-md-4 border-0", class),
23 | tags$div(
24 | class = "card-body",
25 | if (!is.null(title) || !is.null(title_icon)) {
26 | tags$h3(
27 | class = paste("card-title fw-semibold fs-4 mb-3", title_class),
28 | title_icon,
29 | title
30 | )
31 | },
32 | ...
33 | )
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/store/create_card.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags]
3 | )
4 |
5 | #' Create a custom bootstrap card
6 | #'
7 | #' @param title Card title
8 | #' @param title_icon Icon to place before the title. Can be a `shiny::tags$i()`.
9 | #' @param title_class String. Additional bootstrap classes for the title.
10 | #' @param class Additional classes to apply to the card container
11 | #' @param ... Card content
12 | #' @return [shiny::tags$div]
13 | #' @export
14 | create_card <- \(
15 | ...,
16 | title = NULL,
17 | title_icon = NULL,
18 | title_class = NULL,
19 | class = NULL
20 | ) {
21 | tags$div(
22 | class = paste("card p-1 p-md-4 border-0", class),
23 | tags$div(
24 | class = "card-body",
25 | if (!is.null(title) || !is.null(title_icon)) {
26 | tags$h3(
27 | class = paste("card-title fw-semibold fs-4 mb-3", title_class),
28 | title_icon,
29 | title
30 | )
31 | },
32 | ...
33 | )
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/validate_message.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[parse_multipart],
3 | htmltools[tags],
4 | shiny[isTruthy],
5 | .. / store / contact[text_area_input]
6 | )
7 |
8 | #' Validate message sent by user
9 | #'
10 | #' Handler for POST requests at "/contact/message".
11 | #'
12 | #' @return An object of class `shiny.tag`
13 | #' @export
14 | validate_message <- \(req, res) {
15 | body <- parse_multipart(req)
16 | text <- body$msg
17 | html <- check_valid_message(text)
18 | res$send(html)
19 | }
20 |
21 | #' Check if message sent by user is valid
22 | #'
23 | #' @return An object of class `shiny.tag`
24 | #' @export
25 | check_valid_message <- \(message) {
26 | is_valid <- isTruthy(message)
27 |
28 | text_area_input(
29 | value = message,
30 | input_class = if (is_valid) "is-valid" else "is-invalid",
31 | if (!is_valid) {
32 | tags$div(
33 | class = "invalid-feedback",
34 | "Please enter a message"
35 | )
36 | }
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/09_goals/helpers/insert.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | uuid[UUIDgenerate],
3 | . / mongo_query[mongo_query]
4 | )
5 |
6 | #' Insert new document and retrieve its `_id`
7 | #'
8 | #' @param conn Mongo db connection from `mongolite::mongo()`
9 | #' @param data Must be a data.frame, named list (for single record).
10 | #' @param ... Named arguments passed to `conn$insert()`.
11 | #' @return The new document, including its `_id` field.
12 | #' @export
13 | insert <- \(conn, data, ...) {
14 | tmp_oid <- UUIDgenerate(use.time = TRUE, n = 1L)
15 | data$`_tmp_oid` <- tmp_oid
16 | conn$insert(data = data, ...)
17 | # get the inserted document:
18 | doc <- conn$find(
19 | query = mongo_query("_tmp_oid" = tmp_oid),
20 | fields = mongo_query("_tmp_oid" = FALSE)
21 | )
22 | # remove the '_tmp_oid' field:
23 | conn$update(
24 | query = mongo_query("_tmp_oid" = tmp_oid),
25 | update = mongo_query(
26 | "$unset" = list("_tmp_oid" = TRUE)
27 | ),
28 | multiple = TRUE
29 | )
30 | doc
31 | }
32 |
--------------------------------------------------------------------------------
/09_goals/README.md:
--------------------------------------------------------------------------------
1 | ## Goals
2 |
3 | In this example, we will build a CRUD application backend: **Goals**.
4 |
5 | You will be able to **C**reate, **R**ead, **U**pdate & **D**elete Goals.
6 |
7 | Here's what's covered:
8 |
9 | - Ambiorix + MongoDB
10 | - Working with middleware:
11 | - Auth middleware: You will learn how you can use JSON Web Tokens ([JWT](https://jwt.io/)) to protect routes
12 | - Error handling middleware
13 |
14 | ## Prerequisites
15 |
16 | - An installation of the community edition of [MongoDB](https://www.mongodb.com/docs/manual/installation/)
17 | - The [mongolite](https://github.com/jeroen/mongolite) R pkg
18 |
19 | ## Run API
20 |
21 | 1. `cd` into the `09_goals/` dir:
22 |
23 | ```bash
24 | cd 09_goals/
25 | ```
26 |
27 | 1. Fire up R and restore package dependencies:
28 |
29 | ```r
30 | renv::restore()
31 | ```
32 |
33 | 1. `server.R` is the entry point. Run this command in the terminal to start the
34 | API:
35 |
36 | ```bash
37 | Rscript server.R
38 | ```
39 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/README.md:
--------------------------------------------------------------------------------
1 | # Goals
2 |
3 | This is a frontend for example [`09_goals`](../09_goals). Built using `{shiny}`.
4 |
5 | # Features
6 |
7 | - Auth:
8 | - Custom sign up & login pages
9 | - Cookies: Auto-login next time you visit/reload the app
10 | - APIs:
11 | - Make API requests to the backend using `{httr2}`
12 | - Handle any request errors gracefully via `tryCatch()`
13 |
14 | # Run app
15 |
16 | 1. First make sure that the backend is running. See how [here](../09_goals).
17 | 1. `cd` into the dir `12_shiny_frontend_for_09_goals/`:
18 |
19 | ```bash
20 | cd 12_shiny_frontend_for_09_goals/
21 | ```
22 |
23 | 1. Fire up R & restore pkg dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | 1. Add these env vars to your `.Renviron`:
30 |
31 | ```r
32 | BASE_URL = http://127.0.0.1:5000
33 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
34 | ```
35 |
36 | 1. `app.R` is the entry point. Run this on the terminal to start the app:
37 |
38 | ```bash
39 | Rscript app.R
40 | ```
41 |
--------------------------------------------------------------------------------
/14_cors/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | )
4 |
5 | #' Handle GET at '/'
6 | #'
7 | #' @export
8 | hello_get <- \(req, res) {
9 | response <- list(
10 | code = 200L,
11 | msg = "hello, world"
12 | )
13 | res$set_status(200L)$json(response)
14 | }
15 |
16 | #' Allow CORS
17 | #'
18 | #' @details
19 | #' Sets these headers in the response:
20 | #' - `Access-Control-Allow-Methods`
21 | #' - `Access-Control-Allow-Headers`
22 | #' - `Access-Control-Allow-Origin`
23 | #' @export
24 | cors <- \(req, res) {
25 | res$header("Access-Control-Allow-Origin", "http://127.0.0.1:8000")
26 |
27 | if (req$REQUEST_METHOD == "OPTIONS") {
28 | res$header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
29 | res$header(
30 | "Access-Control-Allow-Headers",
31 | req$HEADERS$`access-control-request-headers`
32 | )
33 |
34 | return(
35 | res$set_status(200L)$send("")
36 | )
37 | }
38 | }
39 |
40 | Ambiorix$
41 | new(host = "127.0.0.1", port = 5000L)$
42 | use(cors)$
43 | get("/", hello_get)$
44 | start()
45 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/get_member_by_id.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ./get_db_path[get_db_path],
3 | ambiorix[parse_multipart],
4 | DBI[dbConnect, dbDisconnect, dbReadTable],
5 | RSQLite[SQLite],
6 | dplyr
7 | )
8 |
9 | #' Get member by id controller function
10 | #'
11 | #' @inheritParams handler
12 | #' @name views
13 | #' @keywords internal
14 | #' @export
15 | get_member_by_id <- \(req, res) {
16 | # get the supplied id:
17 | member_id <- req$params$id
18 |
19 | conn <- dbConnect(drv = SQLite(), get_db_path())
20 | on.exit(dbDisconnect(conn))
21 | members <- dbReadTable(conn = conn, name = "members")
22 |
23 | # filter member with that id:
24 | found <- members |> dplyr$filter(id == member_id)
25 |
26 | # if a member with that id was found, return the member:
27 | if (nrow(found) > 0) {
28 | return(res$json(found))
29 | }
30 |
31 | # otherwise, change response status to 400 (Bad Request)
32 | # and provide a message:
33 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
34 | res$set_status(400L)$json(msg)
35 | }
36 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/create_new_member.R:
--------------------------------------------------------------------------------
1 | #' Create new member controller function
2 | #'
3 | #' @inheritParams handler
4 | #' @name views
5 | #' @keywords internal
6 | create_new_member <- \(req, res) {
7 | # parse form-data:
8 | body <- parse_multipart(req)
9 |
10 | name <- body$name
11 | email <- body$email
12 | status <- body$status
13 |
14 | # require all member details:
15 | if (is.null(name) || is.null(email) || is.null(status)) {
16 | msg <- list(msg = "Please include a name, email & status")
17 | return(res$set_status(400L)$json(msg))
18 | }
19 |
20 | # details of the new member:
21 | new_member <- data.frame(
22 | id = uuid::UUIDgenerate(),
23 | name = name,
24 | email = email,
25 | status = status
26 | )
27 |
28 | # save new member:
29 | .GlobalEnv$members <- dplyr::bind_rows(members, new_member)
30 |
31 | # respond with a message and details of the newly created member:
32 | response <- list(
33 | msg = "Member created successfully!",
34 | member = new_member
35 | )
36 |
37 | res$json(response)
38 | }
39 |
--------------------------------------------------------------------------------
/13_parse_raw_json/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[Ambiorix],
3 | webutils[parse_http],
4 | )
5 |
6 | #' Handle POST at '/'
7 | #'
8 | #' @param req Request object.
9 | #' @param res Response object.
10 | #' @return `res$json()`
11 | #' @export
12 | home_post <- \(req, res) {
13 | content_type <- req$CONTENT_TYPE
14 | body <- req$rook.input$read()
15 |
16 | if (length(body) == 0L) {
17 | response <- list(
18 | code = 400L,
19 | msg = "Invalid request"
20 | )
21 |
22 | return(
23 | res$set_status(400L)$json(response)
24 | )
25 | }
26 |
27 | postdata <- parse_http(body, content_type)
28 |
29 | # filter & select as necessary:
30 | row_inds <- iris$Species %in% postdata$species
31 | col_inds <- colnames(iris) %in% postdata$cols
32 | data <- iris[row_inds, col_inds, drop = FALSE]
33 |
34 | response <- list(
35 | code = 200L,
36 | msg = "success",
37 | data = data
38 | )
39 |
40 | res$json(response)
41 | }
42 |
43 | app <- Ambiorix$new(port = 3000, host = "127.0.0.1")
44 |
45 | app$
46 | post("/", home_post)$
47 | start()
48 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/README.md:
--------------------------------------------------------------------------------
1 | ## Dynamic rendering
2 |
3 | When building software, these are the available options:
4 |
5 | 1. Build the backend using ambiorix and the frontend using your favorite framework (React, Angular, Vue, etc.)
6 | 2. Build the backend and frontend using ambiorix
7 |
8 | Let's talk about option 2.
9 |
10 | First things first, you will be rendering html templates/files.
11 | In most cases, you want this to be done dynamically. eg. render a portion of the UI depending on whether a user is an admin or not.
12 |
13 | This is what is referred to as server-side rendering.
14 |
15 | In this example, I use [htmx](https://htmx.org/) to show you how you can build
16 | interactive frontends without touching a single line of JavaScript.
17 |
18 | If you know HTML then you're all set!
19 |
20 | You've already seen how to send HTTP requests to the server & how the server responds (with JSON so far).
21 |
22 | With htmx, your responses from the server will ideally be HTML fragments.
23 |
24 | This works so well with [htmltools](https://rstudio.github.io/htmltools/) you will not believe it!
25 |
--------------------------------------------------------------------------------
/documentation/posts/13_parse_raw_json/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "13: parse raw json"
3 | subtitle: "how to parse raw json in requests"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-29"
6 | categories: [parse_raw_json]
7 | ---
8 |
9 | # Parse raw JSON
10 |
11 | Sometimes you need to parse requests which have raw JSON in the body.
12 | `ambiorix::parse_multipart()` only works for 'form-data'. That's where `webutils::parse_http()` comes in.
13 |
14 | This is a simple example revolving around how you can select columns and filter
15 | rows in the `iris` dataset when the request body is something like this:
16 |
17 | ```json
18 | {
19 | "cols": ["Sepal.Length", "Petal.Width", "Species"],
20 | "species": ["virginica", "setosa"]
21 | }
22 | ```
23 |
24 | # Run API
25 |
26 | 1. `cd` into the `13_parse_raw_json/` dir:
27 |
28 | ```bash
29 | cd 13_parse_raw_json/
30 | ```
31 | 1. Fire up R and restore package dependencies:
32 |
33 | ```r
34 | renv::restore()
35 | ```
36 | 1. `server.R` is the entry point. Run this command in the terminal to start the API:
37 |
38 | ```bash
39 | Rscript server.R
40 | ```
41 |
--------------------------------------------------------------------------------
/documentation/posts/01_hello_world/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "01: hello, world!"
3 | subtitle: "Where else to start? :)"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [01_hello_world]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `01_hello_world/` dir:
12 |
13 | ```bash
14 | cd 01_hello_world/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `index.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | It has 2 endpoints:
41 |
42 | - `/`: [localhost:3000/](http://localhost:3000/)
43 | - `/about`: [localhost:3000/about](http://localhost:3000/about)
44 |
45 | For every other path, it will response with a **404 Not Found**.
46 |
47 | ## Static files
48 |
49 | Learn how to serve ✨[Static Files](../02_static_files/index.qmd)✨ using ambiorix.
50 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/store/toast.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shinytoastr[
3 | toastr_info,
4 | toastr_error,
5 | toastr_success,
6 | toastr_warning,
7 | ]
8 | )
9 |
10 | #' Toast notification with custom defaults
11 | #'
12 | #' @param title String. Title to show on top of the toast.
13 | #' @param message String. Message to show.
14 | #' @param time_out Numeric. How long notification should be kept on screen,
15 | #' in milliseconds. See [shinytoastr::toastr_error()].
16 | #' @param type String. Type of toast. Either "error" (default), "success",
17 | #' "info" or "warning".
18 | #' @export
19 | toast_nofitication <- \(
20 | title = "",
21 | message = "",
22 | time_out = 5000,
23 | type = c("error", "success", "info", "warning")
24 | ) {
25 | type <- match.arg(arg = type)
26 | f <- switch(
27 | EXPR = type,
28 | error = toastr_error,
29 | success = toastr_success,
30 | info = toastr_info,
31 | warning = toastr_warning
32 | )
33 |
34 | f(
35 | title = title,
36 | message = message,
37 | timeOut = time_out,
38 | position = "bottom-center",
39 | progressBar = TRUE,
40 | closeButton = TRUE
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/05_router/r_pkg_structure/R/update_member_info.R:
--------------------------------------------------------------------------------
1 | #' Update member controller function
2 | #'
3 | #' @inheritParams handler
4 | #' @name views
5 | #' @keywords internal
6 | update_member_info <- \(req, res) {
7 | # get the supplied id:
8 | member_id <- req$params$id
9 |
10 | members <- .GlobalEnv$members
11 |
12 | # filter member with that id:
13 | found <- members |> dplyr::filter(id == member_id)
14 |
15 | # if a member with that id is NOT found, change response status
16 | # and provide a message:
17 | if (nrow(found) == 0) {
18 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
19 | return(res$set_status(400L)$json(msg))
20 | }
21 |
22 | # otherwise, proceed to update member:
23 | body <- parse_multipart(req)
24 |
25 | # only update provided fields:
26 | found$name <- body$name %||% found$name
27 | found$email <- body$email %||% found$email
28 | found$status <- body$status %||% found$status
29 |
30 | members[members$id == found$id, ] <- found
31 | .GlobalEnv$members <- members
32 |
33 | response <- list(
34 | msg = "Member updated successfully",
35 | member = found
36 | )
37 | res$json(response)
38 | }
39 |
--------------------------------------------------------------------------------
/08_datatables/store/create_href.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | .. / utils / in_prod[in_prod],
3 | )
4 |
5 | #' Create an anchor tag's href attribute
6 | #'
7 | #' @description
8 | #' Generates the `href` for an anchor (``) tag. If the application is
9 | #' running in a production environment, the given `path` is prefixed to
10 | #' the `href` to ensure the correct base URL is used.
11 | #'
12 | #' @param href String. `href` attribute of an anchor tag (e.g., "/about").
13 | #' @param base_path String. Base path on which the app is deployed. eg.,
14 | #' if the app is deployed at `https://try.ambiorix.dev/infinite-scroll`,
15 | #' the environment variable `APP_BASE_PATH` should be set to `/infinite-scroll`.
16 | #' The default value is obtained from the `APP_BASE_PATH` environment variable,
17 | #' or it can be passed directly.
18 | #'
19 | #' @return String. The complete `href` for the anchor tag.
20 | #'
21 | #' @examples
22 | #' # In production, this may return "/infinite-scroll/about":
23 | #' create_href("/about")
24 | #'
25 | #' @export
26 | create_href <- \(href, base_path = Sys.getenv("APP_BASE_PATH")) {
27 | if (in_prod()) {
28 | href <- paste0(base_path, href)
29 | }
30 | href
31 | }
32 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/delete_member.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ./get_db_path[get_db_path],
3 | DBI[dbConnect, dbDisconnect, dbReadTable, dbWriteTable],
4 | RSQLite[SQLite],
5 | dplyr
6 | )
7 |
8 | #' Delete member controller function
9 | #'
10 | #' @inheritParams handler
11 | #' @name views
12 | #' @keywords internal
13 | #' @export
14 | delete_member <- \(req, res) {
15 | # get the supplied id:
16 | member_id <- req$params$id
17 |
18 | conn <- dbConnect(drv = SQLite(), get_db_path())
19 | on.exit(dbDisconnect(conn))
20 | members <- dbReadTable(conn = conn, name = "members")
21 |
22 | # filter member with that id:
23 | found <- members |> dplyr$filter(id == member_id)
24 |
25 | # if a member with that id is NOT found, change response status
26 | # and provide a message:
27 | if (nrow(found) == 0) {
28 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
29 | return(res$set_status(400L)$json(msg))
30 | }
31 |
32 | # otherwise, proceed to delete member:
33 | members <- members |> dplyr$filter(id != member_id)
34 | dbWriteTable(conn = conn, name = "members", value = members, overwrite = TRUE)
35 |
36 | response <- list(
37 | msg = "Member deleted successfully",
38 | members = members
39 | )
40 | res$json(response)
41 | }
42 |
--------------------------------------------------------------------------------
/documentation/posts/12_frontend_for_09_goals/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "12: frontend for 09_goals"
3 | subtitle: "shiny meets ambiorix"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-26"
6 | categories: [frontend_for_09_goals]
7 | ---
8 |
9 | # Goals
10 |
11 | This is a frontend for example [`09_goals`](../09_goals/index.qmd). Built using `{shiny}`.
12 |
13 | # Features
14 |
15 | - Auth:
16 | - Custom sign up & login pages
17 | - Cookies: Auto-login next time you visit/reload the app
18 | - APIs:
19 | - Make API requests to the backend using `{httr2}`
20 | - Handle any request errors gracefully via `tryCatch()`
21 |
22 | # Run app
23 |
24 | 1. First make sure that the backend is running. See how [here](../09_goals/index.qmd).
25 | 1. `cd` into the dir `12_shiny_frontend_for_09_goals/`:
26 |
27 | ```bash
28 | cd 12_shiny_frontend_for_09_goals/
29 | ```
30 |
31 | 1. Fire up R & restore pkg dependencies:
32 |
33 | ```r
34 | renv::restore()
35 | ```
36 |
37 | 1. Add these env vars to your `.Renviron`:
38 |
39 | ```r
40 | BASE_URL = http://127.0.0.1:5000
41 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
42 | ```
43 |
44 | 1. `app.R` is the entry point. Run this on the terminal to start the app:
45 |
46 | ```bash
47 | Rscript app.R
48 | ```
49 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | reactiveVal,
4 | moduleServer,
5 | observeEvent,
6 | updateTabsetPanel,
7 | freezeReactiveValue,
8 | ],
9 | . / login_server[login_server = server],
10 | . / signup_server[signup_server = server],
11 | .. / .. / helpers / mod[`%||%`],
12 | )
13 |
14 | #' Auth module server
15 | #'
16 | #' @param id String. Module id.
17 | #' @export
18 | server <- \(id) {
19 | moduleServer(
20 | id = id,
21 | module = \(input, output, session) {
22 | switch_to_tab <- \(tab) {
23 | freezeReactiveValue(x = input, name = "tabs")
24 | updateTabsetPanel(
25 | session = session,
26 | inputId = "tabs",
27 | selected = tab
28 | )
29 | }
30 |
31 | rv_user <- reactiveVal()
32 | r_signup <- signup_server(id = "signup")
33 | r_login <- login_server(id = "login")
34 |
35 | observeEvent(r_signup()$go_to_login, switch_to_tab("login"))
36 | observeEvent(r_login()$go_to_signup, switch_to_tab("signup"))
37 |
38 | observeEvent(
39 | eventExpr = c(r_signup()$user, r_login()$user),
40 | handlerExpr = {
41 | user <- r_signup()$user %||% r_login()$user
42 | rv_user(user)
43 | }
44 | )
45 |
46 | return(rv_user)
47 | }
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/create_new_member.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ./get_db_path[get_db_path],
3 | ambiorix[parse_multipart],
4 | RSQLite[SQLite],
5 | DBI[dbConnect, dbDisconnect, dbAppendTable],
6 | uuid
7 | )
8 |
9 | #' Create new member controller function
10 | #'
11 | #' @inheritParams handler
12 | #' @name views
13 | #' @keywords internal
14 | #' @export
15 | create_new_member <- \(req, res) {
16 | # parse form-data:
17 | body <- parse_multipart(req)
18 |
19 | name <- body$name
20 | email <- body$email
21 | status <- body$status
22 |
23 | # require all member details:
24 | if (is.null(name) || is.null(email) || is.null(status)) {
25 | msg <- list(msg = "Please include a name, email & status")
26 | return(res$set_status(400L)$json(msg))
27 | }
28 |
29 | # details of the new member:
30 | new_member <- data.frame(
31 | id = uuid$UUIDgenerate(),
32 | name = name,
33 | email = email,
34 | status = status
35 | )
36 |
37 | # save new member:
38 | conn <- dbConnect(drv = SQLite(), get_db_path())
39 | on.exit(dbDisconnect(conn))
40 | dbAppendTable(conn = conn, name = "members", value = new_member)
41 |
42 | # respond with a message and details of the newly created member:
43 | response <- list(
44 | msg = "Member created successfully!",
45 | member = new_member
46 | )
47 |
48 | res$json(response)
49 | }
50 |
--------------------------------------------------------------------------------
/docs/site_libs/quarto-html/tippy.css:
--------------------------------------------------------------------------------
1 | .tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}
--------------------------------------------------------------------------------
/08_datatables/controllers/flights_get.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | dplyr,
3 | nycflights13[flights],
4 | .. / store / datatable[make_user_friendly_names]
5 | )
6 |
7 | #' Get flights data
8 | #'
9 | #' Handler for GET requests at "/data/flights".
10 | #' @export
11 | flights_get <- \(req, res) {
12 | draw <- req$query$draw
13 | start_row <- as.integer(req$query$start) + 1L # datatables use 0-based indexing (JS)
14 | num_of_rows <- as.integer(req$query$length) - 1L
15 | end_row <- start_row + num_of_rows
16 |
17 | search_res <- flights
18 |
19 | # perform search:
20 | search_value <- req$query$`search[value]`
21 | if (!is.na(search_value)) {
22 | found <- lapply(flights, \(cl) {
23 | grepl(pattern = search_value, x = cl, ignore.case = TRUE)
24 | }) |>
25 | as.data.frame() |>
26 | rowSums(na.rm = TRUE)
27 | found <- which(found > 0)
28 | search_res <- flights |> dplyr$slice(found)
29 | }
30 |
31 | records_filtered <- nrow(search_res)
32 |
33 | # filter out the requested rows:
34 | row_inds <- seq(from = start_row, to = end_row, by = 1L)
35 | filtered <- search_res |> dplyr$slice(row_inds)
36 |
37 | names(filtered) <- names(filtered) |> make_user_friendly_names()
38 |
39 | # datatable expects json:
40 | response <- list(
41 | draw = draw,
42 | recordsTotal = nrow(flights),
43 | recordsFiltered = records_filtered,
44 | data = filtered
45 | )
46 | res$json(response)
47 | }
48 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/validate_email.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[parse_multipart],
3 | htmltools[tags],
4 | .. / store / contact[email_input]
5 | )
6 |
7 | #' Validate contact email
8 | #'
9 | #' Handler for POST requests at "/contact/email".
10 | #'
11 | #' @return An object of class `shiny.tag`
12 | #' @export
13 | validate_email <- \(req, res) {
14 | body <- parse_multipart(req)
15 | email <- body$email
16 | html <- check_valid_email(email)
17 | res$send(html)
18 | }
19 |
20 | #' Check valid email
21 | #'
22 | #' @return An object of class `shiny.tag`
23 | #' @export
24 | check_valid_email <- \(email) {
25 | is_valid <- is_valid_email(email)
26 |
27 | email_input(
28 | value = email,
29 | input_class = if (is_valid) "is-valid" else "is-invalid",
30 | if (!is_valid) {
31 | tags$div(
32 | class = "invalid-feedback",
33 | "Please enter a valid email address"
34 | )
35 | }
36 | )
37 | }
38 |
39 | #' Check if email is valid
40 | #'
41 | #' @param email Character vector. Emails to check.
42 | #' @return Logical vector of same length as `email`.
43 | #' `TRUE` if email is valid, `FALSE` otherwise.
44 | #' @examples
45 | #' mails <- c("example@gmail.com", "this", "S.N@org-nz.com")
46 | #' is_valid_email(mails)
47 | #' @export
48 | is_valid_email <- \(email) {
49 | grepl(
50 | pattern = "\\<[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\>",
51 | x = email,
52 | ignore.case = TRUE
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/controllers/contact_post.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[parse_multipart],
3 | htmltools[tags],
4 | shiny[isTruthy],
5 | . / validate_email[is_valid_email, check_valid_email],
6 | . / validate_message[check_valid_message],
7 | .. / store / contact
8 | )
9 |
10 | #' Receive message sent by user
11 | #'
12 | #' Handler for POST requests at "/contact"
13 | #'
14 | #' @return An object of class `shiny.tag`
15 | #' @export
16 | contact_post <- \(req, res) {
17 | body <- parse_multipart(req)
18 | email <- body$email
19 | firstname <- body$Firstname
20 | lastname <- body$Lastname
21 | message <- body$msg
22 |
23 | all_valid <- is_valid_email(email) &&
24 | isTruthy(message)
25 |
26 | if (all_valid) {
27 | html <- tags$p(
28 | class = "text-success",
29 | "Thank you! We shall happily review your feedback."
30 | )
31 | return(res$send(html))
32 | }
33 |
34 | email_input <- check_valid_email(email)
35 | message_input <- check_valid_message(message)
36 |
37 | html <- tags$form(
38 | `hx-target` = "this",
39 | `hx-post` = "/contact",
40 | `hx-swap` = "outerHTML",
41 | email_input,
42 | tags$div(
43 | class = "row",
44 | tags$div(
45 | class = "col-12 col-md-6",
46 | contact$name_input(kind = "First", value = firstname)
47 | ),
48 | tags$div(
49 | class = "col-12 col-md-6",
50 | contact$name_input(kind = "Last", value = lastname)
51 | )
52 | ),
53 | message_input,
54 | contact$submit_btn()
55 | )
56 | res$send(html)
57 | }
58 |
--------------------------------------------------------------------------------
/05_router/box/routes/api/members/update_member_info.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ./get_db_path[get_db_path],
3 | .. / .. / .. / helpers / operators[`%||%`],
4 | ambiorix[parse_multipart],
5 | DBI[dbConnect, dbDisconnect, dbReadTable, dbWriteTable],
6 | RSQLite[SQLite],
7 | dplyr
8 | )
9 |
10 | #' Update member controller function
11 | #'
12 | #' @inheritParams handler
13 | #' @name views
14 | #' @keywords internal
15 | #' @export
16 | update_member_info <- \(req, res) {
17 | # get the supplied id:
18 | member_id <- req$params$id
19 |
20 | conn <- dbConnect(drv = SQLite(), get_db_path())
21 | on.exit(dbDisconnect(conn))
22 | members <- dbReadTable(conn = conn, name = "members")
23 |
24 | # filter member with that id:
25 | found <- members |> dplyr$filter(id == member_id)
26 |
27 | # if a member with that id is NOT found, change response status
28 | # and provide a message:
29 | if (nrow(found) == 0) {
30 | msg <- list(msg = sprintf("No member with the id of %s", member_id))
31 | return(res$set_status(400L)$json(msg))
32 | }
33 |
34 | # otherwise, proceed to update member:
35 | body <- parse_multipart(req)
36 |
37 | # only update provided fields:
38 | found$name <- body$name %||% found$name
39 | found$email <- body$email %||% found$email
40 | found$status <- body$status %||% found$status
41 |
42 | members[members$id == found$id, ] <- found
43 | dbWriteTable(conn = conn, name = "members", value = members, overwrite = TRUE)
44 |
45 | response <- list(
46 | msg = "Member updated successfully",
47 | member = found
48 | )
49 | res$json(response)
50 | }
51 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/login_server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | . / proxy[
3 | login,
4 | req_error_handler,
5 | ],
6 | shiny[
7 | req,
8 | reactive,
9 | reactiveVal,
10 | moduleServer,
11 | observeEvent,
12 | ],
13 | cookies[set_cookie],
14 | .. / .. / store / mod[toast_nofitication],
15 | )
16 |
17 | #' Login server module
18 | #'
19 | #' @param id String. Module id.
20 | #' @export
21 | server <- \(id) {
22 | moduleServer(
23 | id = id,
24 | module = \(input, output, session) {
25 | rv_user <- reactiveVal()
26 |
27 | observeEvent(input$login, {
28 | email <- input$email
29 | password <- input$password
30 | req(email, password)
31 |
32 | tryCatch(
33 | expr = {
34 | details <- login(
35 | email = email,
36 | password = password
37 | )
38 | rv_user(details$user)
39 |
40 | toast_nofitication(
41 | message = "Logged in!",
42 | type = "success"
43 | )
44 | },
45 | error = req_error_handler
46 | )
47 | })
48 |
49 | # on login, set auth cookie:
50 | observeEvent(rv_user(), {
51 | set_cookie(
52 | cookie_name = "auth",
53 | cookie_value = rv_user()$token,
54 | secure_only = TRUE,
55 | expiration = 30L
56 | )
57 | })
58 |
59 | r_res <- reactive({
60 | list(
61 | go_to_signup = input$signup,
62 | user = rv_user()
63 | )
64 | })
65 | return(r_res)
66 | }
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/15_chat/ui/page.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[...],
3 | )
4 |
5 | #' @export
6 | page <- function(...) {
7 | tags$html(
8 | tags$head(
9 | tags$title("Ambiorix chat"),
10 | tags$link(
11 | rel = "stylesheet",
12 | href = "https://unpkg.com/nes.css@2.3.0/css/nes.min.css"
13 | ),
14 | tags$link(
15 | rel = "stylesheet",
16 | href = "https://fonts.googleapis.com/css?family=Press+Start+2P"
17 | ),
18 | tags$link(
19 | rel = "stylesheet",
20 | href = "static/style.css"
21 | ),
22 | tags$script(
23 | src = "static/ambiorix.js"
24 | ),
25 | tags$script(
26 | src = "static/chat.js",
27 | defer = NA
28 | )
29 | ),
30 | tags$body(
31 | class = "container",
32 | h1("R chat app"),
33 | tags$section(
34 | class = "nes-container mb-1",
35 | id = "chat",
36 | tags$section(
37 | class = "message-list",
38 | id = "chat-list"
39 | )
40 | ),
41 | div(
42 | class = "d-flex",
43 | div(
44 | class = "d-grow",
45 | div(
46 | class = "nes-field mr-1",
47 | tags$input(
48 | id = "message",
49 | class = "nes-input",
50 | placeholder = "Your message"
51 | )
52 | )
53 | ),
54 | div(
55 | class = "d-shrink",
56 | tags$button(
57 | id = "send",
58 | type = "button",
59 | class = "nes-btn is-primary",
60 | "SEND"
61 | )
62 | )
63 | ),
64 | ...
65 | )
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/08_datatables/store/page.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[
3 | tags,
4 | tagList,
5 | HTML,
6 | ],
7 | . / create_href[create_href],
8 | )
9 |
10 | #' Generic HTML page
11 | #'
12 | #' @param ... Passed to the body tag of the html document.
13 | #' @return [htmltools::tags]
14 | #' @export
15 | page <- \(...) {
16 | tagList(
17 | HTML(""),
18 | tags$html(
19 | tags$head(
20 | tags$meta(charset = "utf-8"),
21 | tags$meta(
22 | name = "viewport",
23 | content = "width=device-width, initial-scale=1"
24 | ),
25 | tags$link(
26 | rel = "stylesheet",
27 | href = create_href("/static/styles.css")
28 | ),
29 | tags$link(
30 | rel = "stylesheet",
31 | href = create_href("/static/bootstrap-5.3.2-dist/bootstrap.min.css")
32 | ),
33 | tags$link(
34 | rel = "stylesheet",
35 | href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
36 | ),
37 | tags$link(
38 | rel = "stylesheet",
39 | href = "https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-1.13.8/datatables.min.css"
40 | ),
41 | tags$script(
42 | src = "https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-1.13.8/datatables.min.js"
43 | ),
44 | tags$title("Home | Datatables")
45 | ),
46 | tags$body(
47 | class = "bg-light",
48 | ...,
49 | tags$script(
50 | type = "text/javascript",
51 | src = create_href(
52 | "/static/bootstrap-5.3.2-dist/bootstrap.bundle.min.js"
53 | )
54 | )
55 | )
56 | )
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/signup_server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | req,
4 | reactive,
5 | isTruthy,
6 | reactiveVal,
7 | moduleServer,
8 | observeEvent,
9 | ],
10 | cookies[set_cookie],
11 | . / proxy[
12 | create_account,
13 | req_error_handler,
14 | ],
15 | .. / .. / store / mod[toast_nofitication],
16 | )
17 |
18 | #' Signup server module
19 | #'
20 | #' @param id String. Module id.
21 | #' @export
22 | server <- \(id) {
23 | moduleServer(
24 | id = id,
25 | module = \(input, output, session) {
26 | rv_user <- reactiveVal()
27 |
28 | observeEvent(input$signup, {
29 | name <- input$name
30 | email <- input$email
31 | password <- input$password
32 | req(name, email, password)
33 |
34 | tryCatch(
35 | expr = {
36 | details <- create_account(
37 | name = name,
38 | email = email,
39 | password = password
40 | )
41 | rv_user(details$user)
42 |
43 | toast_nofitication(
44 | title = "Success!",
45 | message = "Account created.",
46 | type = "success"
47 | )
48 | },
49 | error = req_error_handler
50 | )
51 | })
52 |
53 | # on registration, set auth cookie:
54 | observeEvent(rv_user(), {
55 | set_cookie(
56 | cookie_name = "auth",
57 | cookie_value = rv_user()$token,
58 | secure_only = TRUE,
59 | expiration = 30L
60 | )
61 | })
62 |
63 | r_res <- reactive({
64 | list(
65 | go_to_login = input$login,
66 | user = rv_user()
67 | )
68 | })
69 | return(r_res)
70 | }
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/05_router/box/README.md:
--------------------------------------------------------------------------------
1 | ## Using [{box}](https://klmr.me/box/index.html) with [{ambiorix}](https://ambiorix.dev/)
2 |
3 | I highly recommend this way of building apps.
4 |
5 | As you go through the code, you will easily notice how modularized it is.
6 |
7 | {box} enables you to split your app into small, manageable & interconnected modules:
8 |
9 | ```bash
10 | .
11 | ├── data
12 | │ ├── get_db_path.R
13 | │ └── members.sqlite
14 | ├── helpers
15 | │ ├── check_port.R
16 | │ ├── get_port.R
17 | │ └── operators.R
18 | ├── index.R
19 | ├── middleware
20 | │ └── logger.R
21 | ├── models
22 | │ └── members.R
23 | ├── README.md
24 | └── routes
25 | └── api
26 | ├── members
27 | │ ├── controllers.R
28 | │ ├── create_new_member.R
29 | │ ├── delete_member.R
30 | │ ├── get_all_members.R
31 | │ ├── get_db_path.R
32 | │ ├── get_member_by_id.R
33 | │ └── update_member_info.R
34 | └── members.R
35 | ```
36 |
37 | > [!CAUTION]
38 | > You'll notice that I've used an sqlite file. The table "members" is very small,
39 | > that's why I read the whole table and write it again as much as I want.
40 | > Again, this is just for purposes of a reprex.
41 | > In the real world, you should write sql-interpolated queries to the database.
42 | >
43 | > **NEVER** commit a database file or a `.Renviron` file!
44 |
45 | Once you run the app, you'll be able to send requests via these endpoints:
46 | - `GET`: `http://localhost:3000/api/members` : Gets all members
47 | - `GET`: `http://localhost:3000/api/members/:id` : Get a single member by id
48 | - `POST`: `http://localhost:3000/api/members` : Create a new member
49 | - `PUT`: `http://localhost:3000/api/members/:id` : Update member info
50 | - `DELETE`: `http://localhost:3000/api/members/:id` : Delete a member
51 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | req,
4 | isTruthy,
5 | reactiveVal,
6 | observeEvent,
7 | updateTabsetPanel,
8 | freezeReactiveValue,
9 | ],
10 | cookies[get_cookie],
11 | . / auth / mod[
12 | auth_server,
13 | get_account_details,
14 | ],
15 | . / goals / mod[goals_server],
16 | .. / store / mod[toast_nofitication],
17 | )
18 |
19 | #' App server
20 | #'
21 | #' @export
22 | server <- \(input, output, session) {
23 | switch_to_tab <- \(tab) {
24 | freezeReactiveValue(x = input, name = "pages")
25 | updateTabsetPanel(
26 | session = session,
27 | inputId = "pages",
28 | selected = tab
29 | )
30 | }
31 |
32 | rv_user <- reactiveVal()
33 | r_user <- auth_server(id = "auth")
34 | r_goals <- goals_server(id = "goals", rv_user = rv_user)
35 |
36 | observeEvent(r_user(), {
37 | rv_user(r_user())
38 | switch_to_tab("goals")
39 | })
40 |
41 | observeEvent(
42 | eventExpr = get_cookie(cookie_name = "auth"),
43 | handlerExpr = {
44 | token <- get_cookie(cookie_name = "auth")
45 | req(token)
46 |
47 | # if it's signup or login, no need to get user details:
48 | is_signup_or_login <- isTruthy(rv_user()$token)
49 | if (is_signup_or_login) {
50 | return()
51 | }
52 |
53 | tryCatch(
54 | expr = {
55 | details <- get_account_details(token = token)
56 | details$user$token <- token
57 |
58 | rv_user(details$user)
59 | switch_to_tab("goals")
60 | toast_nofitication(
61 | message = "Logged in!",
62 | type = "success"
63 | )
64 | },
65 | error = \(e) {
66 | "Invalid token, not signing in"
67 | }
68 | )
69 | }
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/06_multi_router/README.md:
--------------------------------------------------------------------------------
1 | ## Multiple routers
2 |
3 | You can have multiple routers mounted onto `ambiorix::Ambiorix` via the `use()` method.
4 |
5 | In this example, I show how you can version your api.
6 |
7 | Remember, ambiorix is unopinionated. This is just my way of doing things.
8 |
9 | You'll see that using [{box}](https://github.com/klmr/box) is the easiest way to split up &
10 | organize your files & folders.
11 |
12 | This is the directory structure that I've used:
13 |
14 | ```
15 | .
16 | ├── api
17 | │ ├── members.R
18 | │ ├── v1
19 | │ │ ├── members
20 | │ │ │ ├── controllers.R
21 | │ │ │ ├── create_new_member.R
22 | │ │ │ ├── delete_member.R
23 | │ │ │ ├── get_all_members.R
24 | │ │ │ ├── get_member_by_id.R
25 | │ │ │ └── update_member_info.R
26 | │ │ └── members.R
27 | │ └── v2
28 | │ ├── members
29 | │ │ ├── controllers.R
30 | │ │ ├── create_new_member.R
31 | │ │ ├── delete_member.R
32 | │ │ ├── get_all_members.R
33 | │ │ ├── get_member_by_id.R
34 | │ │ └── update_member_info.R
35 | │ └── members.R
36 | ├── index.R
37 | └── README.md
38 | ```
39 |
40 | This is how `index.R` looks like:
41 |
42 | ```r
43 | box::use(
44 | ambiorix[Ambiorix],
45 | . / api / members
46 | )
47 |
48 | Ambiorix$
49 | new()$
50 | listen(port = 3000L)$
51 | use(members$v1)$ # mount API v1 members' router
52 | use(members$v2)$ # mount API v2 members' router
53 | start(open = FALSE)
54 | ```
55 |
56 | Once you run the app, you should be able to perform requests on
57 | http://localhost:3000/api/v*/members, eg.
58 |
59 | - `GET` request on `http://localhost:3000/api/v1/members`
60 | - `PUT` request on `http://localhost:3000/api/v2/members/:3`
61 |
62 | ... and so on.
63 |
64 | Checkout the routers at:
65 | - [v1 members router](./api/v1/members.R)
66 | - [v2 members router](./api/v2/members.R)
67 |
--------------------------------------------------------------------------------
/15_chat/public/ambiorix.js:
--------------------------------------------------------------------------------
1 | // default insecure
2 | let protocol = 'ws://';
3 |
4 | // upgrade if secure
5 | if (window.location.protocol == "https:")
6 | protocol = 'wss://';
7 |
8 | // get websocket
9 | let ambiorixSocket = new WebSocket(protocol + window.location.host);
10 |
11 | // ambiorix Websocket
12 | class Ambiorix {
13 | constructor() {
14 | this.onOpen = () => { };
15 | this.onClose = () => { };
16 | this.onError = (error) => { console.error(error) };
17 | this._handlers = new Map();
18 | }
19 | // send
20 | static send(name, message) {
21 |
22 | // build message
23 | let msg = { name: name, message: message, isAmbiorix: true };
24 |
25 | ambiorixSocket.send(JSON.stringify(msg));
26 |
27 | }
28 |
29 | onopen(fn) {
30 | this.onOpen = fn;
31 | }
32 |
33 | onclose(fn) {
34 | this.onClose = fn;
35 | }
36 |
37 | onerror(fn) {
38 | this.onError = fn;
39 | }
40 |
41 | start() {
42 | var that = this;
43 | ambiorixSocket.onmessage = (msg) => {
44 | let msgParsed = JSON.parse(msg.data);
45 |
46 | if (!msgParsed.isAmbiorix)
47 | return;
48 |
49 | if (that._handlers.has(msgParsed.name)) {
50 | that._handlers.get(msgParsed.name)(msgParsed.message);
51 | }
52 | }
53 |
54 | ambiorixSocket.onopen = this.onOpen;
55 | ambiorixSocket.onclose = this.onClose;
56 | ambiorixSocket.onerror = this.onError;
57 | }
58 | // receiver
59 | receive(name, fun) {
60 | this._handlers.set(name, fun)
61 | }
62 | }
63 |
64 | // helper function to parseCookies
65 | // parseCookie(document.cookie);
66 | const parseCookie = str => {
67 | if (str == "")
68 | return {};
69 |
70 | return str
71 | .split(';')
72 | .map(v => v.split('='))
73 | .reduce(
74 | (acc, v) => {
75 | acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
76 | return acc;
77 | },
78 | {});
79 | }
80 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/server.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | cli[cli_abort],
3 | cookies[remove_cookie],
4 | shiny[
5 | renderUI,
6 | is.reactive,
7 | moduleServer,
8 | observeEvent,
9 | updateTabsetPanel,
10 | freezeReactiveValue,
11 | ],
12 | . / ui[user_profile_btn],
13 | . / account_server[account_server = server],
14 | . / dashboard_server[dashboard_server = server],
15 | )
16 |
17 | #' Goals server module
18 | #'
19 | #' @param id String. Module id.
20 | #' @param rv_user [shiny::reactiveVal()]. User details after auth.
21 | #' @export
22 | server <- \(id, rv_user) {
23 | if (!is.reactive(rv_user)) {
24 | cli_abort(
25 | message = c(
26 | "x" = "{.var rv_user} must be a {.fn reactiveVal}",
27 | "i" = "Did you pass in a bare value instead of a reactive?"
28 | )
29 | )
30 | }
31 |
32 | moduleServer(
33 | id = id,
34 | module = \(input, output, session) {
35 | ns <- session$ns
36 |
37 | switch_to_tab <- \(tab) {
38 | freezeReactiveValue(x = input, name = "tabs")
39 | updateTabsetPanel(
40 | session = session,
41 | inputId = "tabs",
42 | selected = tab
43 | )
44 | }
45 |
46 | output$user_profile_btn <- renderUI({
47 | user_profile_btn(
48 | ns = ns,
49 | user_name = rv_user()$name
50 | )
51 | })
52 |
53 | observeEvent(input$logout, {
54 | # remove auth cookie, reload page:
55 | remove_cookie(cookie_name = "auth")
56 | session$reload()
57 | })
58 |
59 | observeEvent(input$go_to_account_settings, switch_to_tab("account"))
60 |
61 | dashboard_server(id = "dashboard", rv_user = rv_user)
62 |
63 | r_account <- account_server(id = "account", rv_user = rv_user)
64 | observeEvent(r_account()$go_back_to_dashboard, {
65 | switch_to_tab("dashboard")
66 | })
67 | }
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/documentation/posts/07_dynamic_rendering/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "07: dynamic rendering"
3 | subtitle: "Dynamically render templates"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [07_dynamic_rendering]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `07_dynamic_rendering/` dir:
12 |
13 | ```bash
14 | cd 07_dynamic_rendering/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `index.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | When building software, these are the available options:
41 |
42 | 1. Build the backend using ambiorix and the frontend using your favorite frontend framework (React, Angular, Vue, etc.)
43 | 2. Build both the back and frontend using ambiorix
44 |
45 | Let's talk about option 2.
46 |
47 | First things first, you will be rendering html templates/files.
48 | In most cases, you want this to be done dynamically. eg. render a portion of the UI depending on whether a user is an admin or not.
49 |
50 | This is what is referred to as server-side rendering (SSR).
51 |
52 | In this example, I use [htmx](https://htmx.org/) to show you how you can build
53 | interactive frontends without touching a single line of JavaScript.
54 |
55 | If you know HTML then you're all set!
56 |
57 | You've already seen how to send HTTP requests to the server & how the server responds (with JSON so far).
58 |
59 | With htmx, your responses from the server will ideally be HTML fragments.
60 |
61 | This works so well with [htmltools](https://rstudio.github.io/htmltools/) you will not believe it!
62 |
63 | ## Datatables
64 |
65 | Well, R people love tables. Time for you to look at ✨[datatables](../08_datatables/index.qmd)✨.
66 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/proxy.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | httr2[
3 | request,
4 | req_method,
5 | req_perform,
6 | req_url_path,
7 | last_response,
8 | resp_body_json,
9 | req_body_multipart,
10 | req_auth_bearer_token,
11 | ],
12 | .. / auth / mod[req_error_handler],
13 | .. / .. / helpers / mod[get_base_url],
14 | .. / .. / store / mod[toast_nofitication],
15 | )
16 |
17 | #' Create a new goal
18 | #'
19 | #' @param text String. The goal.
20 | #' @param token String. JWT token.
21 | #' @return Named list.
22 | #' @export
23 | create_goal <- \(text, token) {
24 | request(base_url = get_base_url()) |>
25 | req_url_path("/api/goals") |>
26 | req_auth_bearer_token(token = token) |>
27 | req_body_multipart(text = text) |>
28 | req_perform() |>
29 | resp_body_json()
30 | }
31 |
32 | #' Read all goals
33 | #'
34 | #' @param token String. JWT token.
35 | #' @return Named list.
36 | #' @export
37 | read_goals <- \(token) {
38 | request(base_url = get_base_url()) |>
39 | req_url_path("/api/goals") |>
40 | req_auth_bearer_token(token = token) |>
41 | req_perform() |>
42 | resp_body_json()
43 | }
44 |
45 | #' Update a goal
46 | #'
47 | #' @param id String. Goal id.
48 | #' @param text String. Updated goal.
49 | #' @param token String. JWT token.
50 | #' @return Named list.
51 | #' @export
52 | update_goal <- \(
53 | id,
54 | text,
55 | token
56 | ) {
57 | path <- paste0("/api/goals/", id)
58 | request(base_url = get_base_url()) |>
59 | req_url_path(path) |>
60 | req_auth_bearer_token(token = token) |>
61 | req_method(method = "PUT") |>
62 | req_body_multipart(text = text) |>
63 | req_perform() |>
64 | resp_body_json()
65 | }
66 |
67 | #' Delete a goal
68 | #'
69 | #' @param id String. Goal id.
70 | #' @param token String. JWT token.
71 | #' @return Named list.
72 | #' @export
73 | delete_goal <- \(id, token) {
74 | path <- paste0("/api/goals/", id)
75 | request(base_url = get_base_url()) |>
76 | req_url_path(path) |>
77 | req_auth_bearer_token(token = token) |>
78 | req_method(method = "DELETE") |>
79 | req_perform() |>
80 | resp_body_json()
81 | }
82 |
--------------------------------------------------------------------------------
/09_goals/middleware/auth_middleware.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | jose[jwt_decode_hmac],
3 | .. / config / db[users_conn],
4 | .. / helpers / mongo_query[mongo_query],
5 | .. / helpers / get_jwt_secret[get_jwt_secret]
6 | )
7 |
8 | #' Get protected routes
9 | #'
10 | #' Returns the start string of protected routes
11 | #' @return Character vector.
12 | protected_paths <- \() {
13 | c(
14 | "/api/users/me",
15 | "/api/goals"
16 | )
17 | }
18 |
19 | #' Check if path is protected
20 | #'
21 | #' @return Logical.
22 | is_protected_path <- \(req_path_info) {
23 | startsWith(x = req_path_info, prefix = protected_paths()) |> any()
24 | }
25 |
26 | #' Protect routes
27 | #'
28 | #' @export
29 | protect <- \(req, res) {
30 | # check if route is private:
31 | path_info <- req$PATH_INFO
32 | if (!is_protected_path(path_info)) {
33 | return(res)
34 | }
35 |
36 | auth_headers <- req$HEADERS$authorization
37 | is_valid <- !is.null(auth_headers) &&
38 | startsWith(x = auth_headers, prefix = "Bearer")
39 |
40 | response_401 <- list(
41 | code = 401L,
42 | msg = "Not authorized"
43 | )
44 |
45 | if (!is_valid) {
46 | return(
47 | res$set_status(401L)$json(response_401)
48 | )
49 | }
50 |
51 | tryCatch(
52 | expr = {
53 | # get token from header:
54 | token <- strsplit(x = auth_headers, split = " ")[[1]][[2]]
55 | # verify the token:
56 | decoded <- jwt_decode_hmac(jwt = token, secret = get_jwt_secret())
57 | # get user id from the token:
58 | user_id <- decoded$user_id
59 | # find the user:
60 | found <- users_conn$find(
61 | query = mongo_query(
62 | "_id" = list("$oid" = user_id)
63 | ),
64 | fields = mongo_query(`_id` = TRUE, name = TRUE, email = TRUE)
65 | )
66 | # in case the account was deleted and user tries to access a protected
67 | # resource:
68 | if (nrow(found) != 1L) {
69 | stop("Unauthorized", call. = FALSE)
70 | }
71 | # set the user in the request object:
72 | req$user <- found
73 | },
74 | error = \(e) {
75 | print(e)
76 | res$set_status(401L)$json(response_401)
77 | }
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/08_datatables/store/home.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | nycflights13[flights],
4 | . / nav[nav],
5 | . / page[page],
6 | . / create_card[create_card],
7 | . / create_href[create_href],
8 | . /
9 | datatable[
10 | datatable,
11 | make_user_friendly_names,
12 | ],
13 | )
14 |
15 | #' The "Home" page
16 | #'
17 | #' @return An object of class `shiny.tag`
18 | #' @export
19 | home <- \() {
20 | intro_card <- create_card(
21 | title = "Data Tables",
22 | title_icon = tags$i(class = "bi bi-grid"),
23 | title_class = "text-primary",
24 | class = "shadow-sm",
25 | tags$p("R users love tables, and so do we!"),
26 | tags$p(
27 | "Enter",
28 | tags$a(
29 | href = "https://datatables.net/",
30 | "DataTables."
31 | )
32 | ),
33 | tags$p(
34 | "In the example below, we show how you can use serverside processing
35 | if your data is huge."
36 | )
37 | )
38 |
39 | table_card <- create_card(
40 | title = NULL,
41 | title_icon = NULL,
42 | title_class = "text-primary",
43 | class = "shadow-sm mt-3 mb-5",
44 | # you will mostly get the column names from the database
45 | datatable(
46 | col_names = names(flights) |> make_user_friendly_names(),
47 | table_id = "flights",
48 | table_class = "table-hover table-bordered table-sm",
49 | header_row_class = "table-active",
50 | processing = TRUE,
51 | serverSide = TRUE,
52 | searchDelay = 1500,
53 | # disable sorting of cols:
54 | ordering = FALSE,
55 | # send a GET request (the default for ajax) to "/data/flights"
56 | ajax = create_href("/data/flights"),
57 | columnDefs = list(
58 | list(
59 | # handle missing values:
60 | defaultContent = "",
61 | # avoid cell wrap:
62 | className = "dt-nowrap",
63 | targets = "_all"
64 | )
65 | ),
66 | # enable horizontal scrolling:
67 | scrollX = TRUE
68 | )
69 | )
70 |
71 | container <- tags$div(
72 | class = "container",
73 | intro_card,
74 | table_card
75 | )
76 |
77 | page(
78 | nav(),
79 | container
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | NS,
4 | icon,
5 | uiOutput,
6 | actionLink,
7 | actionButton,
8 | tabsetPanel,
9 | tabPanelBody,
10 | ],
11 | htmltools[tags],
12 | . / account_ui[account_ui = ui],
13 | . / dashboard_ui[dashboard_ui = ui],
14 | )
15 |
16 | #' Create user profile btn
17 | #'
18 | #' @param ns Module namespace from which this function is called.
19 | #' @param user_name String. User name.
20 | #' @return [htmltools::tags$div()]
21 | #' @export
22 | user_profile_btn <- \(ns, user_name) {
23 | tags$div(
24 | class = "dropdown",
25 | actionButton(
26 | inputId = ns("show_account_options"),
27 | class = "dropdown-toggle",
28 | icon = icon(name = NULL, class = "fa fa-user-large"),
29 | `data-bs-toggle` = "dropdown",
30 | `aria-expanded` = "false",
31 | label = user_name
32 | ),
33 | tags$ul(
34 | class = "dropdown-menu",
35 | tags$li(
36 | actionLink(
37 | inputId = ns("logout"),
38 | label = "Logout"
39 | )
40 | ),
41 | tags$li(
42 | actionLink(
43 | inputId = ns("go_to_account_settings"),
44 | label = "Account"
45 | )
46 | )
47 | )
48 | )
49 | }
50 |
51 | #' Goals UI module
52 | #'
53 | #' @param id String. Module id.
54 | #' @return [htmltools::tags$div()]
55 | #' @export
56 | ui <- \(id) {
57 | ns <- NS(id)
58 |
59 | header <- tags$div(
60 | class = "d-flex justify-content-between align-items-center my-4",
61 | tags$h4(
62 | class = "m-0",
63 | tags$a(
64 | class = "text-decoration-none",
65 | href = "/",
66 | "Goals"
67 | )
68 | ),
69 | uiOutput(outputId = ns("user_profile_btn"))
70 | )
71 |
72 | tags$div(
73 | class = "container",
74 | header,
75 | tabsetPanel(
76 | id = ns("tabs"),
77 | type = "hidden",
78 | selected = "dashboard",
79 | tabPanelBody(
80 | value = "dashboard",
81 | dashboard_ui(id = ns("dashboard"))
82 | ),
83 | tabPanelBody(
84 | value = "account",
85 | account_ui(id = ns("account"))
86 | )
87 | )
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/03_basic_routing/README.md:
--------------------------------------------------------------------------------
1 | ## Basic routing
2 |
3 | **Routing** refers to determining how an application responds to
4 | a client request to a particular endpoint, which is a URI (or
5 | path) and a specific HTTP request method (GET, POST, and so on).
6 |
7 | Each route can have one or more handler functions, which are
8 | executed when the route is matched.
9 |
10 | Route definition takes the following structure:
11 |
12 | ```r
13 | app$METHOD(PATH, HANDLER)
14 | ```
15 |
16 | - `app` is an instance of ambiorix.
17 | - `METHOD` is an [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods), in lowercase.
18 | - `HANDLER` is the function executed when the route is matched.
19 |
20 | The following examples illustrate defining simple routes.
21 |
22 | Respond with `Hello World!` on the homepage:
23 |
24 | ```r
25 | app$get("/", \(req, res) {
26 | res$send("Hello World!")
27 | })
28 | ```
29 |
30 | Respond to POST request on the root route (`/`), the application's home page:
31 |
32 | ```r
33 | app$post("/", \(req, res) {
34 | res$send("Got a POST request")
35 | })
36 | ```
37 |
38 | Respond to a PUT request to the `/user` route:
39 |
40 | ```r
41 | app$put("/user", \(req, res) {
42 | res$send("Got a PUT request at /user")
43 | })
44 | ```
45 |
46 | Respond to a DELETE request to the `/user` route:
47 |
48 | ```r
49 | app$delete("/user", \(req, res) {
50 | res$send("Got a DELETE request at /user")
51 | })
52 | ```
53 |
54 | **NOTE:**
55 | - Browsers issue a GET request by default. This means that if you run the [example app](./index.R) and visit localhost 3000, you'll only be able to access the homepage (`/`).
56 | - The easiest ways to see the responses from the other routes would be to either:
57 | - Install [Postman](https://www.postman.com/) and make the requests from there (**Recommended**). The free tier is more than enough.
58 | Here is how issuing the various requests above would look like:
59 | 
60 | 
61 | 
62 | 
63 | - Open another R session and use [httr2](https://httr2.r-lib.org/index.html) to make requests to the endpoints.
64 |
--------------------------------------------------------------------------------
/15_chat/public/chat.js:
--------------------------------------------------------------------------------
1 | const icon = (() => {
2 | const x = [
3 | "ash",
4 | "bcrikko",
5 | "bulbasaur",
6 | "charmander",
7 | "kirby",
8 | "mario",
9 | "logo",
10 | "octocat",
11 | "pokeball",
12 | "squirtle",
13 | ];
14 | return x[Math.floor(Math.random() * x.length)];
15 | })();
16 |
17 | document.addEventListener("DOMContentLoaded", function() {
18 | const id = createId();
19 |
20 | const wss = new Ambiorix();
21 | wss.onopen(() => {
22 | console.info("Connecting");
23 | });
24 |
25 | wss.receive("chat", (msg) => {
26 | // it was sent by me
27 | if (msg.id == id) return;
28 | insertLeft(msg.text, msg.icon);
29 | });
30 |
31 | wss.onclose(() => {
32 | console.error("Disconnected");
33 | });
34 |
35 | wss.start();
36 |
37 | handleChat(id);
38 | });
39 |
40 | const createId = () => {
41 | return Math.random().toString(16).slice(2);
42 | }
43 |
44 | const handleChat = (id) => {
45 | const btn = document.querySelector("#send");
46 | btn.addEventListener("click", (_event) => {
47 | const tgt = document.querySelector("#message")
48 | const text = tgt.value;
49 | tgt.value = "";
50 |
51 | if (!text || text == "") return;
52 |
53 | insertRight(text);
54 | Ambiorix.send("chat", { text: text, id: id, icon: icon });
55 | });
56 |
57 | const query = document.querySelector("#message");
58 | query.addEventListener("keydown", (event) => {
59 | if (event.key != "Enter") return;
60 |
61 | const btn = document.querySelector("#send");
62 | btn.click();
63 | })
64 | }
65 |
66 | const insertLeft = (message, icon) => {
67 | document.querySelector("#chat-list").insertAdjacentHTML("beforeend", chatLeft(message, icon))
68 | }
69 |
70 | const insertRight = (message) => {
71 | document.querySelector("#chat-list").insertAdjacentHTML("beforeend", chatRight(message))
72 | }
73 |
74 | const chatLeft = (message, icon) => {
75 | return ``;
81 | }
82 |
83 | const chatRight = (message) => {
84 | return ``;
90 | }
91 |
--------------------------------------------------------------------------------
/08_datatables/store/datatable.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | jsonlite[toJSON],
4 | stringr[str_replace_all, str_to_title]
5 | )
6 |
7 | #' Generate html to create a datatable
8 | #'
9 | #' Meant to be used for serverside rendering of [datatables](https://datatables.net/)
10 | #'
11 | #' @param col_names Column names of the data.
12 | #' @param table_id Table id.
13 | #' @param table_class Bootstrap classes to apply to the table.
14 | #' @param ... Named `key=value` pairs. Table options. Don't include 'columns'.
15 | #' See [datatable options](https://datatables.net/manual/options).
16 | #' @return An object of class `shiny.tag`
17 | #' @export
18 | datatable <- \(
19 | col_names,
20 | table_id,
21 | table_class = NULL,
22 | header_row_class = NULL,
23 | ...
24 | ) {
25 | tagList(
26 | tags$table(
27 | id = table_id,
28 | class = paste("table", table_class),
29 | tags$thead(
30 | class = header_row_class,
31 | tags$tr(
32 | lapply(col_names, \(col_name) {
33 | tags$th(scope = "col", col_name)
34 | })
35 | )
36 | ),
37 | tags$tbody()
38 | ),
39 | tags$script(
40 | datatable_script(
41 | id = table_id,
42 | col_names = col_names,
43 | ...
44 | )
45 | )
46 | )
47 | }
48 |
49 | #' Make a script to convert a table to datatable
50 | #'
51 | #' @param id Table id.
52 | #' @param col_names Column names of the data.
53 | #' @param ... Named `key=value` pairs. Table options. Don't include 'columns'.
54 | #' See [datatable options](https://datatables.net/manual/options).
55 | #' @examples
56 | #' script <- datatable_script(
57 | #' id = "datatable",
58 | #' col_names = names(iris),
59 | #' processing = TRUE,
60 | #' serverSide = TRUE,
61 | #' ajax = create_href("/api/data")
62 | #' )
63 | #'
64 | #' cat(script, "\n")
65 | #' @return String. JavaScript code.
66 | #' @export
67 | datatable_script <- \(id, col_names, ...) {
68 | columns <- lapply(col_names, \(name) list(data = name))
69 | opts <- list(
70 | ...,
71 | columns = columns
72 | ) |>
73 | toJSON(auto_unbox = TRUE, pretty = TRUE)
74 | sprintf(
75 | "$(document).ready(function() {
76 | $('#%s').DataTable(%s);
77 | });",
78 | id,
79 | opts
80 | )
81 | }
82 |
83 | #' Make user friendly names
84 | #'
85 | #' @param names Names to make user-friendly
86 | #' @return Character vector of same length as `names`
87 | #' @export
88 | make_user_friendly_names <- \(names) {
89 | names |>
90 | str_replace_all(pattern = "_", replacement = " ") |>
91 | str_to_title()
92 | }
93 |
--------------------------------------------------------------------------------
/02_static_files/README.md:
--------------------------------------------------------------------------------
1 | ## Serving static files in ambiorix
2 |
3 | To serve static files such as images, CSS files, and JavaScript
4 | files, use the `app$static()` method.
5 |
6 | For example, let's say this is your directory structure:
7 |
8 | ```
9 | |- index.R
10 | |- public/
11 | |- index.html
12 | |- about.html
13 | |- image.jpg
14 | |- css
15 | |- styles.css
16 | |- index2.R
17 | |- main.js
18 | ```
19 |
20 | ### `/public`
21 |
22 | To make the files accessible at the path `/public`, you'd do this:
23 |
24 | ```r
25 | app$static(path = "public", uri = "public")
26 | ```
27 |
28 | - `path` specifies the static directory.
29 | - `uri` defines the path ambiorix should serve the static files from.
30 |
31 | So now you'll be able to do this in your app:
32 |
33 | ```r
34 | app$get("/", \(req, res) {
35 | res$send(
36 | "Hello everyone!
37 |
"
38 | )
39 | })
40 | ```
41 |
42 | Also, note that you can access every static resource by navigating to it from the browser eg. [http://localhost:3000/public/image.jpg](http://localhost:3000/public/image.jpg)
43 |
44 | ### `/your-own-path`
45 |
46 | You can make static content accessible via your own custom path too.
47 |
48 | For example, let's use `/static` this time:
49 |
50 | ```r
51 | app$static(path = "public", uri = "static")
52 | ```
53 |
54 | Then in your code you'll use this:
55 |
56 | ```r
57 | app$get("/", \(req, res) {
58 | res$send(
59 | "Hello everyone!
60 |
"
61 | )
62 | })
63 | ```
64 |
65 | ## Serve regular files
66 |
67 | Take a look at:
68 | - [public/about.html](public/about.html)
69 | - [public/index.html](public/index.html)
70 | - [public/index2.R](public/index2.R)
71 |
72 | Now run the app ([index.R](index.R)) and navigate to these links in your browser:
73 | - http://localhost:3000/static/index.html
74 | - http://localhost:3000/static/about.html
75 | - http://localhost:3000/static/index2.R
76 |
77 | By making the `public/` folder static, any resource placed there can be
78 | accessed via the browser.
79 |
80 | This is usually not what you're going to use ambiorix for. For
81 | the most part you're going to either:
82 | - Build APIs so that you can connect from a frontend like React et al, OR,
83 | - Render templates where you can insert dynamic data rather than just having a static website.
84 |
85 | ## Keep this in mind:
86 |
87 | - All static files are exposed and can be accessed via the browser. DO NOT put sensitive files there.
88 | - You can also use [htmltools](https://rstudio.github.io/htmltools/index.html) tags instead of writing html strings.
89 |
--------------------------------------------------------------------------------
/05_router/README.md:
--------------------------------------------------------------------------------
1 | ## Routing
2 |
3 | **Routing** refers to how an application's endpoints (URIs) respond to client requests.
4 |
5 | For an introduction to routing, see [03_basic_routing](../03_basic_routing/).
6 |
7 | If you look at [04_simple_json_api/index.R](../04_simple_json_api/index.R) you'll notice that the routes we created all belong to `/api/members` and we kept repeating that base/root route.
8 |
9 | Wouldn't it be nice to only have to use `/` and `/:id` and have ambiorix prepend the `/api/members` automatically?
10 |
11 | That would give you a better app structure making it manageable.
12 |
13 | Enter **ambiorix::Router()**.
14 |
15 | Use the `ambiorix::Router` class to create modular, mountable router handlers.
16 |
17 | A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app".
18 |
19 | Using the example [04_simple_json_api/index.R](../04_simple_json_api/index.R), this is how we would transform it:
20 |
21 | ```r
22 | # members.R
23 | members_router <- \() {
24 | router <- Router$new("/members")
25 |
26 | # get all members:
27 | router$get("/", \(req, res) {
28 | # ...
29 | })
30 |
31 | # get a single member:
32 | router$get("/:id", \(req, res) {
33 | # ...
34 | })
35 |
36 | # create a new member:
37 | router$post("/", \(req, res)) {
38 | # ...
39 | }
40 |
41 | # update member:
42 | router$put("/:id", \(req, res) {
43 | # ...
44 | })
45 |
46 | # delete member:
47 | router$delete("/:id", \(req, res) {
48 | # ...
49 | })
50 |
51 | router
52 | }
53 | ```
54 |
55 | The `index.R` file would now be:
56 |
57 | ```r
58 | library(ambiorix)
59 |
60 | #
61 |
62 | PORT <- 3000
63 |
64 | app <- Ambiorix$new()
65 |
66 | # mount the router:
67 | app$use(members_router())
68 |
69 | app$start(port = PORT, open = FALSE)
70 | ```
71 |
72 | > [!NOTE]
73 | > Ambiorix is unopinionated. As such, it is up to you to decide how you want to bring/export the `members.R` file into `index.R`.
74 | > Some options are:
75 | > - Use [box](https://klmr.me/box/) (**Highly recommended, ⭐⭐⭐⭐⭐**) especially if you develop large apps/systems, for two reasons:
76 | > 1. Allows nested files, folders & modules (a big win)
77 | > 2. Explicit name imports ie. you're almost always sure from which package a function is from.
78 | > - Use R package structure (**Recommended, ⭐⭐⭐⭐**). Will not allow nested folders but will work really well for small to medium apps.
79 | > - `source()` files (**NOT recommended, 1⭐**). Haha. Iykyk.
80 |
81 | I provide 2 examples:
82 | - One [using box](./box/), and,
83 | - Another [using standard R package structure](./r_pkg_structure/)
84 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/store/inputs.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | icon,
4 | textInput,
5 | passwordInput,
6 | actionButton
7 | ],
8 | htmltools[
9 | tags,
10 | tagList,
11 | tagQuery,
12 | ]
13 | )
14 |
15 | #' Text input
16 | #'
17 | #' @param ... Named arguments passed to [shiny::textInput()]
18 | #' @param required Logical. Whether to add the html attribute 'required' to the
19 | #' input.
20 | #' @return [htmltools::tags()]
21 | #' @export
22 | text_input <- \(..., required = TRUE) {
23 | required <- if (required) NA
24 |
25 | tag_q <- textInput(..., width = "100%") |> tagQuery()
26 | tag_q$
27 | find("input")$
28 | addAttrs(required = required)$
29 | allTags()
30 | }
31 |
32 | #' Email input
33 | #'
34 | #' @param ... Named arguments passed to [shiny::textInput()]
35 | #' @param required Logical. Whether to add the html attribute 'required' to the
36 | #' input.
37 | #' @return [htmltools::tags()]
38 | #' @export
39 | email_input <- \(..., required = TRUE) {
40 | required <- if (required) NA
41 |
42 | tag_q <- textInput(..., width = "100%") |> tagQuery()
43 | tag_q$
44 | find("input")$
45 | removeAttrs("type")$
46 | addAttrs(type = "email", required = required)$
47 | allTags()
48 | }
49 |
50 | #' Password input
51 | #'
52 | #' @param ns Module namespace from which this function is called.
53 | #' @param input_id String. Input id of the password. Don't wrap in `ns()`.
54 | #' Defaults to "password".
55 | #' @param label String, [htmltools::tags()]. Label of the input. Defaults to
56 | #' "Password".
57 | #' @param required Logical. Whether to add the html attribute 'required' to the
58 | #' input.
59 | #' @return [htmltools::tags()]
60 | #' @export
61 | password_input <- \(
62 | ns,
63 | input_id = "password",
64 | label = "Password",
65 | required = TRUE
66 | ) {
67 | required <- if (required) NA
68 | input_id <- ns(input_id)
69 |
70 | btn_id <- sprintf("toggle_%s", input_id) |> ns()
71 | btn <- actionButton(
72 | inputId = btn_id,
73 | label = NULL,
74 | icon = icon(name = "eye")
75 | )
76 |
77 | toggle_password_script <- tags$script(
78 | sprintf(
79 | "toggle_password('%s', '%s')",
80 | input_id,
81 | btn_id
82 | )
83 | )
84 |
85 | tag_q <- passwordInput(
86 | inputId = input_id,
87 | label = label,
88 | width = "100%"
89 | ) |> tagQuery()
90 |
91 | tag_q$find("input")$addAttrs(required = required)
92 | input <- tags$div(
93 | class = "input-group",
94 | tag_q$find("input")$selectedTags(),
95 | btn
96 | )
97 | tag_q$find("input")$replaceWith(input)
98 |
99 | tagList(
100 | tag_q$allTags(),
101 | toggle_password_script
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/08_datatables/store/nav.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags]
3 | )
4 |
5 | #' Navbar navigation links
6 | #'
7 | #' @param href A character vector. eg. c("/", "/about" "/contact")
8 | #' @param label A character vector of same length as `href`.
9 | #' Labels for the navigation links. eg. c("Home", "About", "Contact").
10 | #' @param active A string. The current page. One of the options from
11 | #' what you supplied to `label`.
12 | #' @details Checkout [bootstrap nav](https://getbootstrap.com/docs/5.3/components/navbar/#nav) for more info
13 | #' @examples
14 | #' \dontrun{
15 | #' nav(
16 | #' href = c("/", "/about", "/contact"),
17 | #' label = c("Home", "About", "Contact"),
18 | #' active = "Home"
19 | #' )
20 | #' }
21 | #' @return An object of class `shiny.tag`
22 | #' @export
23 | nav <- \(
24 | href = c("/"),
25 | label = c("Home"),
26 | active = "Home"
27 | ) {
28 | nav_items <- lapply(seq_along(href), \(i) {
29 | class <- paste("nav-link", if (label[i] == active) "active border-bottom border-dark")
30 | aria_current <- if (label[i] == active) "page" else NULL
31 | tags$li(
32 | class = "nav-item",
33 | tags$a(
34 | class = class,
35 | `aria-current` = aria_current,
36 | href = href[i],
37 | label[i]
38 | )
39 | )
40 | })
41 |
42 | title <- "Axim"
43 | title_class <- "navbar-brand card-title text-uppercase fw-bold"
44 | tags$nav(
45 | class = "navbar navbar-expand-lg sticky-top mb-3 bg-white shadow-sm",
46 | tags$div(
47 | class = "container",
48 | tags$a(class = title_class, href = "/", title),
49 | tags$button(
50 | class = "navbar-toggler",
51 | type = "button",
52 | `data-bs-toggle` = "offcanvas",
53 | `data-bs-target` = "#offcanvasNavbar",
54 | `aria-controls` = "offcanvasNavbar",
55 | `aria-label` = "Toggle navigation",
56 | tags$span(class = "navbar-toggler-icon")
57 | ),
58 | tags$div(
59 | id = "offcanvasNavbar",
60 | class = "offcanvas offcanvas-end",
61 | tabindex = "-1",
62 | `aria-labelledby` = "offcanvasNavbarLabel",
63 | tags$div(
64 | class = "offcanvas-header",
65 | tags$h5(
66 | id = "offcanvasNavbarLabel",
67 | class = paste("offcanvas-title", title_class),
68 | title
69 | ),
70 | tags$button(
71 | class = "btn-close",
72 | type = "button",
73 | `data-bs-dismiss` = "offcanvas",
74 | `aria-label` = "Close"
75 | )
76 | ),
77 | tags$div(
78 | class = "offcanvas-body",
79 | tags$ul(
80 | class = "navbar-nav justify-content-end flex-grow-1 pe-3",
81 | nav_items
82 | )
83 | )
84 | )
85 | )
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/documentation/posts/06_multi_router/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "06: multi-router"
3 | subtitle: "Mount multiple routers"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [06_multi_router]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `06_multi_router/` dir:
12 |
13 | ```bash
14 | cd 06_multi_router/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `server.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | You can have multiple routers mounted onto `ambiorix::Ambiorix` via the `use()` method.
41 |
42 | In this example, I show how you can version your api.
43 |
44 | Remember, ambiorix is unopinionated. This is just my way of doing things.
45 |
46 | You'll see that using [{box}](https://github.com/klmr/box) is the easiest way to split up &
47 | organize your files & folders.
48 |
49 | This is the directory structure that I've used:
50 |
51 | ```
52 | .
53 | ├── api
54 | │ ├── members.R
55 | │ ├── v1
56 | │ │ ├── members
57 | │ │ │ ├── controllers.R
58 | │ │ │ ├── create_new_member.R
59 | │ │ │ ├── delete_member.R
60 | │ │ │ ├── get_all_members.R
61 | │ │ │ ├── get_member_by_id.R
62 | │ │ │ └── update_member_info.R
63 | │ │ └── members.R
64 | │ └── v2
65 | │ ├── members
66 | │ │ ├── controllers.R
67 | │ │ ├── create_new_member.R
68 | │ │ ├── delete_member.R
69 | │ │ ├── get_all_members.R
70 | │ │ ├── get_member_by_id.R
71 | │ │ └── update_member_info.R
72 | │ └── members.R
73 | ├── index.R
74 | └── README.md
75 | ```
76 |
77 | This is how `server.R` looks like:
78 |
79 | ```r
80 | box::use(
81 | ambiorix[Ambiorix],
82 | . / api / members
83 | )
84 |
85 | Ambiorix$
86 | new()$
87 | listen(port = 3000L)$
88 | use(members$v1)$ # mount API v1 members' router
89 | use(members$v2)$ # mount API v2 members' router
90 | start(open = FALSE)
91 | ```
92 |
93 | Once you run the app, you should be able to perform requests on
94 | `http://localhost:3000/api/v*/members`, eg.
95 |
96 | - `GET` request on `http://localhost:3000/api/v1/members`
97 | - `PUT` request on `http://localhost:3000/api/v2/members/:3`
98 |
99 | ... and so on.
100 |
101 | Checkout the routers in these files:
102 |
103 | - `./api/v1/members.R`
104 | - `./api/v2/members.R`
105 |
106 | ## Dynamic rendering
107 |
108 | Are you ready for some frontend fun? See ✨[dynamic rendering](../07_dynamic_rendering/index.qmd)✨.
109 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/store/nav.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags]
3 | )
4 |
5 | #' Navbar navigation links
6 | #'
7 | #' @param href A character vector. eg. c("/", "/about" "/contact")
8 | #' @param label A character vector of same length as `href`.
9 | #' Labels for the navigation links. eg. c("Home", "About", "Contact").
10 | #' @param active A string. The current page. One of the options from
11 | #' what you supplied to `label`.
12 | #' @details Checkout [bootstrap nav](https://getbootstrap.com/docs/5.3/components/navbar/#nav) for more info
13 | #' @examples
14 | #' \dontrun{
15 | #' nav(
16 | #' href = c("/", "/about", "/contact"),
17 | #' label = c("Home", "About", "Contact"),
18 | #' active = "Home"
19 | #' )
20 | #' }
21 | #' @return An object of class `shiny.tag`
22 | #' @export
23 | nav <- \(
24 | href = c("/", "/about", "/contact"),
25 | label = c("Home", "About", "Contact"),
26 | active = "Home"
27 | ) {
28 | nav_items <- lapply(seq_along(href), \(i) {
29 | class <- paste("nav-link", if (label[i] == active) "active border-bottom border-dark")
30 | aria_current <- if (label[i] == active) "page" else NULL
31 | tags$li(
32 | class = "nav-item",
33 | tags$a(
34 | class = class,
35 | `aria-current` = aria_current,
36 | # `data-bs-dismiss` = "offcanvas",
37 | href = href[i],
38 | label[i]
39 | )
40 | )
41 | })
42 |
43 | title <- "Axim"
44 | title_class <- "navbar-brand card-title text-uppercase fw-bold"
45 | tags$nav(
46 | class = "navbar navbar-expand-lg sticky-top mb-3 bg-white shadow-sm",
47 | tags$div(
48 | class = "container",
49 | tags$a(class = title_class, href = "/", title),
50 | tags$button(
51 | class = "navbar-toggler",
52 | type = "button",
53 | `data-bs-toggle` = "offcanvas",
54 | `data-bs-target` = "#offcanvasNavbar",
55 | `aria-controls` = "offcanvasNavbar",
56 | `aria-label` = "Toggle navigation",
57 | tags$span(class = "navbar-toggler-icon")
58 | ),
59 | tags$div(
60 | id = "offcanvasNavbar",
61 | class = "offcanvas offcanvas-end",
62 | tabindex = "-1",
63 | `aria-labelledby` = "offcanvasNavbarLabel",
64 | tags$div(
65 | class = "offcanvas-header",
66 | tags$h5(
67 | id = "offcanvasNavbarLabel",
68 | class = paste("offcanvas-title", title_class),
69 | title
70 | ),
71 | tags$button(
72 | class = "btn-close",
73 | type = "button",
74 | `data-bs-dismiss` = "offcanvas",
75 | `aria-label` = "Close"
76 | )
77 | ),
78 | tags$div(
79 | class = "offcanvas-body",
80 | tags$ul(
81 | class = "navbar-nav justify-content-end flex-grow-1 pe-3",
82 | nav_items
83 | )
84 | )
85 | )
86 | )
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/documentation/about.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "About"
3 | about:
4 | template: marquee
5 | links:
6 | - icon: github
7 | text: Github
8 | href: https://github.com/ambiorix-web/ambiorix
9 | - icon: book
10 | text: docs
11 | href: https://ambiorix.dev/
12 | ---
13 |
14 | ## What is ambiorix?
15 |
16 | [Ambiorix](https://ambiorix.dev/) is an unopinionated and minimalist web framework for R.
17 |
18 | It is inspired by [express.js](https://expressjs.com/). In fact,
19 | the syntax is almost identical, only that it is R and not JavaScript.
20 |
21 | Ambiorix is a **server-side** or **back-end** framework. It is not
22 | comparable to client-side frameworks like React, Angular, Vue, etc. However, it can be used in combination with those frameworks
23 | to build full stack applications.
24 |
25 | ## "Unopinionated and Minimalist"... What does that mean?
26 |
27 | Ambiorix does not assume you're going to build your API (or app) in any
28 | certain way or using a certain design pattern. You have absolute
29 | full control of how you handle requests to the server and
30 | how you respond.
31 |
32 | ## So, why should I use ambiorix?
33 |
34 | - Makes building web applications with R **VERY** easy
35 | - Used for both server rendered apps as well as API/Microservices
36 | - Full control of the request and response cycle
37 | - Great to use with your favorite client side framework (whether React, Angular, Vue etc.)
38 |
39 | ## What exciting features can I look forward to?
40 |
41 | Here are the features that make ambiorix well-suited to building
42 | large systems & applications:
43 |
44 | - Out of the box **routing**
45 | - Creating a robust **API** is quick and easy
46 | - Templating (html, markdown, etc.)
47 | - Websockets in case you need bidirectional communication
48 |
49 | ## What should I know first?
50 |
51 | The main prerequisites are:
52 |
53 | - having a good understanding of **R fundamentals**, and,
54 | - basic knowledge of HTTP status codes.
55 |
56 | You can pickup most of the concepts on the go.
57 |
58 | ## Package dependencies used in examples
59 |
60 | Each example has been bootstrapped using [`{renv}`](https://rstudio.github.io/renv/articles/renv.html) to ease reproducibility.
61 |
62 | To install the dependencies for a specific example eg. `05_router`:
63 |
64 | 1. Switch to its directory
65 |
66 | ```bash
67 | cd 05_router
68 | ```
69 |
70 | 1. Add this to the `.Renviron` file:
71 |
72 | ```r
73 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
74 | ```
75 |
76 | 1. Fire up R and restore the dependencies:
77 |
78 | ```r
79 | renv::restore()
80 | ```
81 |
82 | ## Note
83 |
84 | In some of the examples I've committed the `.Renviron` files. I did this just to make it easier for you to run the examples.
85 |
86 | However, you should **NEVER** commit a file that has any type of credentials (`.Renviron` in this case). Such files should always be included in your `.gitignore` so that git doesn't track them.
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## What is ambiorix?
2 |
3 | [Ambiorix](https://ambiorix.dev/) is an unopinionated and minimalist web framework for R.
4 |
5 | It is inspired by [express.js](https://expressjs.com/). In fact,
6 | the syntax is almost identical, only that it is R and not JavaScript.
7 |
8 | Ambiorix is a **server-side** or **back-end** framework. It is not
9 | comparable to client-side frameworks like React, Angular, Vue, etc. However, it can be used in combination with those frameworks
10 | to build full stack applications.
11 |
12 | ## "Unopinionated and Minimalist"... What does that mean?
13 |
14 | Ambiorix does not assume you're going to build your app/API in any
15 | certain way or using a certain design pattern. You have absolute
16 | full control of how you handle requests to the server and
17 | how you respond.
18 |
19 | ## So, why should I use ambiorix?
20 |
21 | - Makes building web applications with R **VERY** easy
22 | - Used for both server rendered apps (SSRs) as well as API/Microservices
23 | - Full control of the request and response cycle
24 | - Great to use with your favorite client side framework (whether React, Angular, Vue etc.)
25 |
26 | ## What exciting features can I look forward to?
27 |
28 | Here are the features that make ambiorix well-suited to building
29 | large systems & applications:
30 |
31 | - Out of the box **routing**
32 | - Creating a robust **API** is quick and easy
33 | - Templating (HTML, markdown, pug, etc.)
34 | - Websockets in case you need bidirectional communication
35 |
36 | ## What should I know first?
37 |
38 | The main prerequisites are:
39 |
40 | - having a good understanding of **R fundamentals**, and,
41 | - basic knowledge of HTTP status codes.
42 |
43 | You can pickup most of the concepts on the go.
44 |
45 | ## Package dependencies used in examples
46 |
47 | Each example has been bootstrapped using [`{renv}`](https://rstudio.github.io/renv/articles/renv.html) to ease reproducibility.
48 |
49 | To install the dependencies for a specific example eg. `05_router`:
50 |
51 | 1. Switch to its directory
52 |
53 | ```bash
54 | cd 05_router
55 | ```
56 |
57 | 1. Add this to the `.Renviron` file:
58 |
59 | ```r
60 | RENV_CONFIG_SANDBOX_ENABLED = FALSE
61 | ```
62 |
63 | 1. Fire up R and restore the dependencies:
64 |
65 | ```r
66 | renv::restore()
67 | ```
68 |
69 | ## Documentation
70 |
71 | You can find documentation on each of the examples at [ambiorix-web/ambiorix-examples](https://ambiorix-web.github.io/ambiorix-examples/).
72 |
73 | The documentation includes:
74 |
75 | - how to run each example
76 | - code and concept explanations where necessary
77 |
78 | ## Note
79 |
80 | In some of the examples I've committed the `.Renviron` files. I did this just to make it easier for you to run the examples.
81 |
82 | However, you should **NEVER** commit a file that has any type of credentials (`.Renviron` in this case). Such files should always be included in your `.gitignore` so that git doesn't track them.
83 |
--------------------------------------------------------------------------------
/documentation/posts/02_static_files/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "02: static files"
3 | subtitle: "Serve static files"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [02_static_files]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `02_static_files/` dir:
12 |
13 | ```bash
14 | cd 02_static_files/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `index.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | It has a single endpoint:
41 |
42 | - `/`: [localhost:3000/](http://localhost:3000/)
43 |
44 | Now that the app is running, navigate to these links in your browser:
45 |
46 | - [localhost:3000/static/index.html](http://localhost:3000/static/index.html)
47 | - [localhost:3000/static/about.html](http://localhost:3000/static/about.html)
48 | - [localhost:3000/static/index2.R](http://localhost:3000/static/index2.R)
49 |
50 | To serve static files such as images, CSS files, JavaScript
51 | files etc., use the `app$static()` method.
52 |
53 | For example, let's say this is your directory structure:
54 |
55 | ```
56 | |- index.R
57 | |- public/
58 | |- index.html
59 | |- about.html
60 | |- image.jpg
61 | |- css
62 | |- styles.css
63 | |- index2.R
64 | |- main.js
65 | ```
66 |
67 | ### `/public`
68 |
69 | To make the files accessible at the path `/public`, you'd do this:
70 |
71 | ```r
72 | app$static(path = "public", uri = "public")
73 | ```
74 |
75 | - `path` specifies the static directory.
76 | - `uri` defines the path ambiorix should serve the static files from.
77 |
78 | So now you'll be able to do this in your app:
79 |
80 | ```r
81 | app$get("/", \(req, res) {
82 | res$send(
83 | "Hello everyone!
84 |
"
85 | )
86 | })
87 | ```
88 |
89 | By making the `public/` folder static, any resource placed there can be
90 | accessed via the browser eg. http://localhost:3000/public/image.jpg
91 |
92 | ### `/your-own-path`
93 |
94 | You can make static content accessible via your own custom path too.
95 |
96 | For example, let's use `/static` this time:
97 |
98 | ```r
99 | app$static(path = "public", uri = "static")
100 | ```
101 |
102 | Then in your code you'll use this:
103 |
104 | ```r
105 | app$get("/", \(req, res) {
106 | res$send(
107 | "Hello everyone!
108 |
"
109 | )
110 | })
111 | ```
112 |
113 | ## Keep this in mind:
114 |
115 | - All static files are exposed and can be accessed via the browser. DO NOT put sensitive files there.
116 | - You can also use [htmltools](https://rstudio.github.io/htmltools/index.html) tags instead of writing html strings.
117 |
118 | ## Basic routing
119 |
120 | Learn the ✨[basics of routing](../03_basic_routing/index.qmd)✨.
121 |
--------------------------------------------------------------------------------
/documentation/posts/03_basic_routing/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "03: basic routing"
3 | subtitle: "An intro to routes"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [03_basic_routing]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `03_basic_routing/` dir:
12 |
13 | ```bash
14 | cd 03_basic_routing/
15 | ```
16 |
17 | 1. Fire up R:
18 |
19 | ```bash
20 | R
21 | ```
22 |
23 | 1. Restore package dependencies:
24 |
25 | ```r
26 | renv::restore()
27 | ```
28 |
29 | Once done, exit R.
30 | 1. `index.R` is the entry point. To start the app, run this on the terminal:
31 |
32 | ```bash
33 | Rscript index.R
34 | ```
35 |
36 | ## Explanation
37 |
38 | This app starts a server and listens on port 3000 for connections.
39 |
40 | It has two endpoints:
41 |
42 | - `/`
43 | - `/user`
44 |
45 | **Routing** refers to determining how an application responds to
46 | a client request to a particular endpoint, which is a URI (or
47 | path) and a specific HTTP request method (GET, POST, and so on).
48 |
49 | Each route can have one or more handler functions, which are
50 | executed when the route is matched.
51 |
52 | Route definition takes the following structure:
53 |
54 | ```r
55 | app$METHOD(PATH, HANDLER)
56 | ```
57 |
58 | - `app` is an instance of ambiorix.
59 | - `METHOD` is an [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods), in lowercase.
60 | - `HANDLER` is the function executed when the route is matched.
61 |
62 | The following examples illustrate defining simple routes.
63 |
64 | Respond with `Hello World!` on the homepage:
65 |
66 | ```r
67 | app$get("/", \(req, res) {
68 | res$send("Hello World!")
69 | })
70 | ```
71 |
72 | Respond to POST request on the root route (`/`), the application's home page:
73 |
74 | ```r
75 | app$post("/", \(req, res) {
76 | res$send("Got a POST request")
77 | })
78 | ```
79 |
80 | Respond to a PUT request to the `/user` route:
81 |
82 | ```r
83 | app$put("/user", \(req, res) {
84 | res$send("Got a PUT request at /user")
85 | })
86 | ```
87 |
88 | Respond to a DELETE request to the `/user` route:
89 |
90 | ```r
91 | app$delete("/user", \(req, res) {
92 | res$send("Got a DELETE request at /user")
93 | })
94 | ```
95 |
96 | ## Keep this in mind:
97 |
98 | - Browsers issue a GET request by default. This means that once you run this example and visit localhost 3000, you'll only be able to access the homepage (`/`).
99 | - The easiest way to see the responses from the other routes would be to either:
100 | - Install [Postman](https://www.postman.com/) and make the requests from there (**Recommended**). The free tier is more than enough.
101 | Here is how issuing the various requests above would look like:
102 | 
103 | 
104 | 
105 | 
106 | - Open another R session and use [httr2](https://httr2.r-lib.org/index.html) to make requests to the endpoints.
107 |
108 | ## A simple JSON API
109 |
110 | You're now ready to build ✨[a simple JSON API](../04_simple_json_api/index.qmd)✨.
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/auth/proxy.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | httr2[
3 | request,
4 | req_method,
5 | req_perform,
6 | req_url_path,
7 | last_response,
8 | resp_body_json,
9 | req_body_multipart,
10 | req_auth_bearer_token,
11 | ],
12 | .. / .. / helpers / mod[get_base_url],
13 | .. / .. / store / mod[toast_nofitication],
14 | )
15 |
16 | #' Create user account
17 | #'
18 | #' @param name String. Name of user.
19 | #' @param email String. User email.
20 | #' @param password String. Password.
21 | #' @return Named list.
22 | #' @export
23 | create_account <- \(name, email, password) {
24 | user_details <- list(
25 | name = name,
26 | email = email,
27 | password = password
28 | )
29 |
30 | request(base_url = get_base_url()) |>
31 | req_url_path("/api/users") |>
32 | req_body_multipart(!!!user_details) |>
33 | req_perform() |>
34 | resp_body_json()
35 | }
36 |
37 | #' Login user
38 | #'
39 | #' @param email String. User email.
40 | #' @param password String. Password.
41 | #' @return Named list.
42 | #' @export
43 | login <- \(email, password) {
44 | user_details <- list(
45 | email = email,
46 | password = password
47 | )
48 |
49 | request(base_url = get_base_url()) |>
50 | req_url_path("/api/users/login") |>
51 | req_body_multipart(!!!user_details) |>
52 | req_perform() |>
53 | resp_body_json()
54 | }
55 |
56 | #' Get user account details
57 | #'
58 | #' @param token String. JWT token.
59 | #' @return Named list.
60 | #' @export
61 | get_account_details <- \(token) {
62 | request(base_url = get_base_url()) |>
63 | req_url_path("/api/users/me") |>
64 | req_auth_bearer_token(token = token) |>
65 | req_perform() |>
66 | resp_body_json()
67 | }
68 |
69 | #' Update user account details
70 | #'
71 | #' @param name String. Name of user.
72 | #' @param email String. User email.
73 | #' @param password String. Password.
74 | #' @param token String. JWT token.
75 | #' @return Named list.
76 | #' @export
77 | update_account_details <- \(
78 | name = NULL,
79 | email = NULL,
80 | password = NULL,
81 | token
82 | ) {
83 | new_details <- list(
84 | name = name,
85 | email = email,
86 | password = password
87 | )
88 |
89 | request(base_url = get_base_url()) |>
90 | req_url_path("/api/users/me") |>
91 | req_auth_bearer_token(token = token) |>
92 | req_method(method = "PUT") |>
93 | req_body_multipart(!!!new_details) |>
94 | req_perform() |>
95 | resp_body_json()
96 | }
97 |
98 | #' Delete user account
99 | #'
100 | #' @param token String. JWT token.
101 | #' @return Named list.
102 | #' @export
103 | delete_account <- \(token) {
104 | request(base_url = get_base_url()) |>
105 | req_url_path("/api/users/me") |>
106 | req_auth_bearer_token(token = token) |>
107 | req_method(method = "DELETE") |>
108 | req_perform() |>
109 | resp_body_json()
110 | }
111 |
112 | #' Request error handler
113 | #'
114 | #' @param e Error object. See [stop()].
115 | #' @export
116 | req_error_handler <- \(e) {
117 | error <- last_response() |> resp_body_json()
118 | print(error)
119 | toast_nofitication(
120 | title = "Error!",
121 | message = error$msg,
122 | type = "error"
123 | )
124 | }
125 |
--------------------------------------------------------------------------------
/documentation/posts/14_cors/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "14: CORS"
3 | subtitle: "how to allow CORS in ambiorix"
4 | author: "Kennedy Mwavu"
5 | date: "2024-08-24"
6 | categories: [CORS]
7 | ---
8 |
9 | You're probably reading this because you've hit that CORS error in your browser :)
10 |
11 | TL;DR: See the [middleware](https://github.com/ambiorix-web/ambiorix-examples/tree/main/14_cors).
12 |
13 | # Quoting [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS):
14 |
15 | **Cross-Origin Resource Sharing** (CORS) is an HTTP-header based mechanism that
16 | allows a server to indicate any origins (domain, scheme, or port) than its own
17 | from which a browser should permit loading resources.
18 |
19 | CORS also relies on a mechanism by which browsers make a "preflight" request
20 | to the server hosting the cross-origin resource, in order to check that the
21 | server will permit the actual request. In that preflight, the browser sends
22 | the headers that indicate the HTTP method and headers that will be used in the
23 | actual request.
24 |
25 | An example of cross-origin request: the front-end JavaScript code served from
26 | `https://domain-a.com` uses `fetch()` to make a request for `https://domain-b.com/data.json`.
27 |
28 | For security reasons, browsers restrict cross-origin HTTP requests initiated
29 | from scripts.
30 |
31 | For example, `fetch()` and `XMLHtttpRequest` follow the 'same-origin policy'.
32 | This means that a web application using those APIs can only request resources
33 | from the same origin the application was loaded from unless the response
34 | from the other origins includes the right CORS headers.
35 |
36 | # How to allow CORS
37 |
38 | It's really simple. This image from [html5rocks.com](https://www.html5rocks.com/static/images/cors_server_flowchart.png) is all you need:
39 |
40 | 
41 |
42 | # Middleware
43 |
44 | As [Rihanna and Calvin Harris](https://www.youtube.com/watch?v=kOkQ4T5WO9E) would tell you, This Is What You Came For:
45 |
46 | ```r
47 | #' Allow CORS
48 | #'
49 | #' @details
50 | #' Sets these headers in the response:
51 | #' - `Access-Control-Allow-Methods`
52 | #' - `Access-Control-Allow-Headers`
53 | #' - `Access-Control-Allow-Origin`
54 | #' @export
55 | cors <- \(req, res) {
56 | res$header("Access-Control-Allow-Origin", "http://127.0.0.1:8000")
57 |
58 | if (req$REQUEST_METHOD == "OPTIONS") {
59 | res$header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
60 | res$header(
61 | "Access-Control-Allow-Headers",
62 | req$HEADERS$`access-control-request-headers`
63 | )
64 |
65 | return(
66 | res$set_status(200L)$send("")
67 | )
68 | }
69 | }
70 | ```
71 |
72 | Change the values set for the headers to suit your specific use-case.
73 |
74 | If your API requires the use of cookies or authentication tokens across
75 | domains, you will also need to set the "Access-Control-Allow-Credentials"
76 | header inside the `if () {}` block:
77 |
78 | ```r
79 | res$header("Access-Control-Allow-Credentials", "true")
80 | ```
81 |
82 | # Note
83 |
84 | - DO NOT include a trailing slash in the allowed origins.
85 | - `http://127.0.0.1:8000/`❌
86 | - `http://127.0.0.1:8000`✅
87 | - In Ambiorix, middleware is executed in the order it is registered with
88 | `use()`. Make sure this middleware is the first one in the sequence.
89 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/dashboard_ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | NS,
4 | uiOutput,
5 | textInput,
6 | modalDialog,
7 | actionButton,
8 | ],
9 | htmltools[tags],
10 | reactable[reactableOutput],
11 | .. / .. / store / mod[
12 | text_input,
13 | center_modal,
14 | ],
15 | )
16 |
17 | #' Create new goal UI
18 | #'
19 | #' @param ns Module namespace from which this function is called.
20 | #' @return [htmltools::tags$div()]
21 | #' @export
22 | new_goal_ui <- \(ns) {
23 | tags$form(
24 | id = ns("new_goal_form"),
25 | tags$div(
26 | class = "d-flex",
27 | text_input(
28 | inputId = ns("new_goal"),
29 | label = NULL,
30 | placeholder = "Enter goal here"
31 | ),
32 | actionButton(
33 | inputId = ns("create"),
34 | class = "ms-1 mb-3",
35 | label = "Create",
36 | type = "submit"
37 | )
38 | )
39 | )
40 | }
41 |
42 | #' Goal modal UI
43 | #'
44 | #' Can be used as both the edit or delete goal modal.
45 | #' @param ns Module namespace from which this function is called.
46 | #' @param type String. Type of modal. Either "edit" (default) or "delete".
47 | #' @param text String. Current text value of goal. Defaults to `NULL`.
48 | #' @return [shiny::modalDialog()]
49 | #' @export
50 | goal_modal <- \(
51 | ns,
52 | type = c("edit", "delete"),
53 | text = NULL
54 | ) {
55 | type <- match.arg(arg = type)
56 | is_edit <- identical(type, "edit")
57 | cancel_id <- paste0("cancel_", type)
58 | confirm_id <- paste0("confirm_", type)
59 | confirm_label <- if (is_edit) "Save" else "Confirm"
60 | title <- paste(
61 | if (is_edit) "Edit" else "Delete",
62 | "goal"
63 | )
64 |
65 | modalDialog(
66 | title = NULL,
67 | footer = NULL,
68 | size = "m",
69 | easyClose = TRUE,
70 | tags$h5(
71 | class = "mb-3",
72 | title
73 | ),
74 | if (is_edit) {
75 | textInput(
76 | inputId = ns("edited_goal"),
77 | label = NULL,
78 | value = text,
79 | width = "100%"
80 | )
81 | },
82 | tags$div(
83 | class = "d-flex justify-content-between",
84 | actionButton(
85 | inputId = ns(cancel_id),
86 | label = "Cancel"
87 | ),
88 | actionButton(
89 | inputId = ns(confirm_id),
90 | label = confirm_label
91 | )
92 | )
93 | ) |>
94 | center_modal()
95 | }
96 |
97 | #' View goals UI
98 | #'
99 | #' @param ns Module namespace from which this function is called.
100 | #' @return [htmltools::tags$div()]
101 | #' @export
102 | view_goals_ui <- \(ns) {
103 | tags$div(
104 | reactableOutput(outputId = ns("goals")),
105 | tags$div(
106 | id = ns("btn_container"),
107 | class = "d-none justify-content-between my-2",
108 | actionButton(
109 | inputId = ns("edit"),
110 | label = "Edit"
111 | ),
112 | actionButton(
113 | inputId = ns("delete"),
114 | label = "Delete"
115 | )
116 | )
117 | )
118 | }
119 |
120 | #' Dashboard UI module
121 | #'
122 | #' @param id String. Module id.
123 | #' @return [htmltools::tags$div()]
124 | #' @export
125 | ui <- \(id) {
126 | ns <- NS(id)
127 |
128 | tags$div(
129 | new_goal_ui(ns = ns),
130 | view_goals_ui(ns = ns)
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/documentation/posts/05_router/index.qmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "05: router"
3 | subtitle: "What's a router?"
4 | author: "Kennedy Mwavu"
5 | date: "2024-07-10"
6 | categories: [05_router]
7 | ---
8 |
9 | ## Run app
10 |
11 | 1. `cd` into the `05_router/` dir:
12 |
13 | ```bash
14 | cd 05_router/
15 | ```
16 |
17 | - There are 2 directories there: `box/` & `r_pkg_structure/`.
18 | - `cd` into any of them, say, `r_pkg_structure/`:
19 |
20 | ```bash
21 | cd r_pkg_structure/
22 | ```
23 |
24 | 1. Fire up R:
25 |
26 | ```bash
27 | R
28 | ```
29 |
30 | 1. Restore package dependencies:
31 |
32 | ```r
33 | renv::restore()
34 | ```
35 |
36 | Once done, exit R.
37 | 1. `server.R` is the entry point. To start the app, run this on the terminal:
38 |
39 | ```bash
40 | Rscript index.R
41 | ```
42 |
43 | ## Explanation
44 |
45 | This app starts a server and listens on port 3000 for connections.
46 |
47 | It has two endpoints:
48 |
49 | - `/api/members`
50 | - `/api/members/:id`
51 |
52 | **Routing** refers to how an application's endpoints (URIs) respond to client requests.
53 |
54 | For an introduction to routing, see [03_basic_routing](../03_basic_routing/index.qmd).
55 |
56 | If you look at [04_simple_json_api](../04_simple_json_api/index.qmd) you'll notice that the routes we created all belong to `/api/members` and we kept repeating that base/root route.
57 |
58 | Wouldn't it be nice to only have to use `/` and `/:id` and have ambiorix prepend the `/api/members` automatically?
59 |
60 | That would give you a better app structure making it manageable.
61 |
62 | ## Enter `ambiorix::Router()`.
63 |
64 | Use the `ambiorix::Router` class to create modular, mountable router handlers.
65 |
66 | A `Router` instance is a complete middleware and routing system; for this reason, it is often referred to as a "mini-app".
67 |
68 | Using the example [04_simple_json_api](../04_simple_json_api/index.qmd), this is how we would transform it:
69 |
70 | ```r
71 | # members.R
72 | members_router <- \() {
73 | router <- Router$new("/members")
74 |
75 | # get all members:
76 | router$get("/", \(req, res) {
77 | # ...
78 | })
79 |
80 | # get a single member:
81 | router$get("/:id", \(req, res) {
82 | # ...
83 | })
84 |
85 | # create a new member:
86 | router$post("/", \(req, res)) {
87 | # ...
88 | }
89 |
90 | # update member:
91 | router$put("/:id", \(req, res) {
92 | # ...
93 | })
94 |
95 | # delete member:
96 | router$delete("/:id", \(req, res) {
97 | # ...
98 | })
99 |
100 | router
101 | }
102 | ```
103 |
104 | The `server.R` file would now be:
105 |
106 | ```r
107 | library(ambiorix)
108 |
109 | #
110 |
111 | PORT <- 3000
112 |
113 | app <- Ambiorix$new()
114 |
115 | # mount the router:
116 | app$use(members_router())
117 |
118 | app$start(port = PORT, open = FALSE)
119 | ```
120 |
121 | ## Keep this in mind:
122 |
123 | Ambiorix is unopinionated. As such, it is up to you to decide how you want to bring/export the `members.R` file into `index.R`.
124 |
125 | Some options are:
126 |
127 | - Use [box](https://klmr.me/box/) (**Highly recommended, ⭐⭐⭐⭐⭐**) especially if you develop large apps/systems, for two reasons:
128 | 1. Allows nested files, folders & modules (a big win)
129 | 2. Explicit name imports ie. you're almost always sure from which package a function is from.
130 | - Use R package structure (**Recommended, ⭐⭐⭐⭐**). Will not allow nested folders but will work really well for small to medium apps.
131 | - `source()` files (**NOT recommended, 1⭐**). Haha. Iykyk.
132 |
133 | Choose wisely.
134 |
135 | ## Multiple routers
136 |
137 | Learn how you can mount ✨[multiple routers](../06_multi_router/index.qmd)✨.
138 |
139 |
--------------------------------------------------------------------------------
/09_goals/controllers/goal_controller.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | ambiorix[parse_multipart],
3 | .. / config / db[goals_conn],
4 | .. / helpers / mongo_query[mongo_query],
5 | .. / helpers / to_json[to_json],
6 | .. / helpers / insert[insert]
7 | )
8 |
9 | #' Get goals
10 | #'
11 | #' GET `/api/goals`. Private access.
12 | #' @export
13 | get_goals <- \(req, res) {
14 | # remember we set the user on the req object at `protect()`:
15 | goals <- goals_conn$find(
16 | query = mongo_query(user_id = req$user$`_id`),
17 | fields = mongo_query("_id" = TRUE, text = TRUE)
18 | )
19 |
20 | response <- list(goals = goals)
21 |
22 | res$json(response)
23 | }
24 |
25 | #' Set a goal
26 | #'
27 | #' POST `/api/goals`. Private access.
28 | #' @export
29 | set_goal <- \(req, res) {
30 | body <- parse_multipart(req)
31 | text <- body$text
32 |
33 | if (is.null(text)) {
34 | response <- list(
35 | code = 400L,
36 | msg = "Please add a 'text' field in the body"
37 | )
38 | return(
39 | res$set_status(400L)$json(response)
40 | )
41 | }
42 |
43 | goal <- data.frame(user_id = req$user$`_id`, text = text)
44 | doc <- insert(conn = goals_conn, data = goal)
45 | response <- list(
46 | code = 201L,
47 | msg = "Success.",
48 | goal = as.list(doc)
49 | )
50 |
51 | res$set_status(201L)$json(response)
52 | }
53 |
54 | #' Update goal
55 | #'
56 | #' PUT `/api/goals/:id`. Private access.
57 | #' @export
58 | update_goal <- \(req, res) {
59 | id <- req$params$id
60 | goal <- tryCatch(
61 | expr = goals_conn$find(
62 | query = mongo_query(
63 | user_id = req$user$`_id`,
64 | "_id" = list("$oid" = id)
65 | ),
66 | fields = mongo_query("_id" = TRUE, text = TRUE)
67 | ),
68 | error = \(e) {
69 | print(e)
70 | data.frame()
71 | }
72 | )
73 |
74 | if (nrow(goal) == 0) {
75 | response <- list(
76 | code = 400L,
77 | msg = "Goal not found"
78 | )
79 | return(
80 | res$set_status(400L)$json(response)
81 | )
82 | }
83 |
84 | body <- parse_multipart(req)
85 | text <- body$text
86 |
87 | goals_conn$update(
88 | query = mongo_query(
89 | user_id = req$user$`_id`,
90 | "_id" = list("$oid" = id)
91 | ),
92 | update = mongo_query(
93 | "$set" = list(text = text)
94 | )
95 | )
96 |
97 | updated_goal <- goals_conn$find(
98 | query = mongo_query(
99 | user_id = req$user$`_id`,
100 | "_id" = list("$oid" = id)
101 | ),
102 | fields = mongo_query("_id" = TRUE, text = TRUE)
103 | )
104 |
105 | response <- list(
106 | code = 200L,
107 | msg = "Goal updated successfully",
108 | goal = as.list(updated_goal)
109 | )
110 | res$json(response)
111 | }
112 |
113 | #' Delete goal
114 | #'
115 | #' DELETE `/api/goals/:id`. Private access.
116 | #' @export
117 | delete_goal <- \(req, res) {
118 | id <- req$params$id
119 | goal <- tryCatch(
120 | expr = goals_conn$find(
121 | query = mongo_query(
122 | user_id = req$user$`_id`,
123 | "_id" = list("$oid" = id)
124 | ),
125 | fields = mongo_query("_id" = TRUE, text = TRUE)
126 | ),
127 | error = \(e) {
128 | print(e)
129 | data.frame()
130 | }
131 | )
132 |
133 | if (nrow(goal) == 0) {
134 | msg <- list(
135 | code = 400L,
136 | msg = "Goal not found"
137 | )
138 | return(
139 | res$set_status(400L)$json(msg)
140 | )
141 | }
142 |
143 | goals_conn$remove(
144 | query = mongo_query(
145 | user_id = req$user$`_id`,
146 | "_id" = list("$oid" = id)
147 | )
148 | )
149 |
150 | response <- list(
151 | code = 200L,
152 | msg = "Goal deleted successfully",
153 | goal = as.list(goal)
154 | )
155 |
156 | res$json(response)
157 | }
158 |
--------------------------------------------------------------------------------
/10_live_reloading/README.md:
--------------------------------------------------------------------------------
1 | ## Live Reloading
2 |
3 | When building applications (whether backend or frontend), it gets tiring to manually stop & restart the app when you make changes to the source code.
4 |
5 | This guide shows you how you can monitor files for changes and have the app restart automatically.
6 |
7 | > [!NOTE]
8 | > This is not something specific to ambiorix, but it is extremely useful during my development process, so I saw it a good idea to have it included as part of the examples.
9 |
10 | ## Prerequisites
11 |
12 | - A working installation of [nodejs](https://nodejs.org/en) & [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
13 |
14 | In case you'd like to know, we won't be writing any JavaScript.
15 |
16 | ## Setup
17 |
18 | 1. You of course need to have an app. You can use any of the code in the previous examples. We will use a simple `server.R` file, which is also the entrypoint:
19 | ```R
20 | library(ambiorix)
21 |
22 | app <- Ambiorix$new()
23 |
24 | app$get("/", \(req, res){
25 | res$send("Using {ambiorix}!")
26 | })
27 |
28 | app$get("/about", \(req, res){
29 | res$text("About")
30 | })
31 |
32 | app$start()
33 | ```
34 |
35 | 2. Change to your project's root dir.
36 |
37 | ```bash
38 | cd myproject
39 | ```
40 | 3. Initialize an npm project & create the `package.json` file:
41 |
42 | ```bash
43 | npm init -y
44 | ```
45 | The `-y` flag accepts the default npm setup.
46 | 4. Install [`nodemon`](https://www.npmjs.com/package/nodemon) as a dev dependency:
47 | ```bash
48 | npm i -D nodemon
49 | ```
50 | 1. Create the file `nodemon.json` at the root dir of your project and paste this in it:
51 | ```bash
52 | {
53 | "execMap": {
54 | "R": "Rscript"
55 | },
56 | "ext": "*"
57 | }
58 | ```
59 | This specifies an executable mapping for `.R` files: `Rscript`.
60 |
61 | It also tells nodemon to monitor all files (`*`) for changes.
62 | You can as well monitor specific file extensions. For example to only watch `.R`, `.html`, `.css` & `.js` files, change `ext` to:
63 |
64 | ```bash
65 | "ext": "R,html,css,js"
66 | ```
67 | 1. Open `package.json` and edit the "scripts" section to this:
68 | ```bash
69 | "scripts": {
70 | "dev": "nodemon --signal SIGTERM server.R"
71 | }
72 | ```
73 | This tells nodemon to re-run the file `server.R` when changes happen.
74 |
75 | The `--signal SIGTERM` is basically telling nodemon to send a termination signal to the previously running program before spawning a new one. This is especially useful for freeing the port the app is running on, and then re-using it again.
76 | 1. Run the app:
77 | ```bash
78 | npm run dev
79 | ```
80 | This runs the `dev` script which starts, stops & restarts your app when changes occurs.
81 |
82 | Now try making some changes to your source code and enjoy the experience.
83 |
84 | 2. When working on the backend, you don't want a browser tab to open each time the app is restarted since you will mostly be sending requests via postman, so you will set `start(open = FALSE)` in your `server.R`:
85 | ```R
86 | library(ambiorix)
87 |
88 | app <- Ambiorix$new()
89 |
90 | app$get("/", \(req, res){
91 | res$send("Using {ambiorix}!")
92 | })
93 |
94 | app$get("/about", \(req, res){
95 | res$text("About")
96 | })
97 |
98 | app$start(open = FALSE)
99 | ```
100 | 1. To stop npm, press `CTRL` + `C`.
101 | 1. Add `node_modules/` to your `.gitignore` file.
102 | 2. You can as well add `nodemon.json`, `package-lock.json`, & `package.json` to `.gitignore` since they're just used for development purposes. I have commited them for this example just so you can see their contents.
103 |
--------------------------------------------------------------------------------
/12_shiny_frontend_for_09_goals/modules/goals/account_ui.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | shiny[
3 | NS,
4 | tagList,
5 | uiOutput,
6 | textInput,
7 | modalDialog,
8 | actionButton,
9 | ],
10 | htmltools[tags],
11 | reactable[reactableOutput],
12 | .. / .. / store / mod[
13 | center_modal,
14 | text_input,
15 | email_input,
16 | password_input,
17 | ],
18 | )
19 |
20 | #' Edit user field modal UI
21 | #'
22 | #' @param ns Module namespace from which this function is called.
23 | #' @param field String. The field to edit. Either "name" (default),
24 | #' "email" or "password".
25 | #' @param text String. Current text value of the field. Defaults
26 | #' to `NULL`.
27 | #' @return [shiny::modalDialog()]
28 | #' @export
29 | edit_field_modal <- \(
30 | ns,
31 | field = c("name", "email", "password"),
32 | text = NULL
33 | ) {
34 | field <- match.arg(arg = field)
35 | prev_id <- paste0("prev_", field)
36 | new_id <- paste0("new_", field)
37 | label <- paste("New", field)
38 |
39 | inputs <- switch(
40 | EXPR = field,
41 | password = tagList(
42 | password_input(
43 | ns = ns,
44 | input_id = prev_id,
45 | label = "Current password"
46 | ),
47 | password_input(
48 | ns = ns,
49 | input_id = new_id,
50 | label = label
51 | )
52 | ),
53 | email = email_input(
54 | inputId = ns(new_id),
55 | label = label,
56 | value = text
57 | ),
58 | name = text_input(
59 | inputId = ns(new_id),
60 | label = label,
61 | value = text
62 | )
63 | )
64 |
65 | btns <- tags$div(
66 | class = "d-flex justify-content-between",
67 | actionButton(
68 | inputId = ns("cancel_edit"),
69 | label = "Cancel"
70 | ),
71 | actionButton(
72 | inputId = ns("confirm_edit"),
73 | label = "Save"
74 | )
75 | )
76 |
77 | modalDialog(
78 | title = NULL,
79 | footer = NULL,
80 | size = "m",
81 | easyClose = TRUE,
82 | inputs,
83 | btns
84 | ) |>
85 | center_modal()
86 | }
87 |
88 | #' Delete account modal UI
89 | #'
90 | #' @param ns Module namespace from which this function is called.
91 | #' @return [shiny::modalDialog()]
92 | #' @export
93 | delete_account_modal <- \(ns) {
94 | btns <- tags$div(
95 | class = "d-flex justify-content-between",
96 | actionButton(
97 | inputId = ns("cancel_deletion"),
98 | label = "Cancel"
99 | ),
100 | actionButton(
101 | inputId = ns("confirm_deletion"),
102 | label = "Confirm"
103 | )
104 | )
105 |
106 | modalDialog(
107 | title = NULL,
108 | footer = NULL,
109 | size = "m",
110 | easyClose = TRUE,
111 | tags$h5("Delete account"),
112 | tags$p("Are you sure you want to delete your account?"),
113 | btns
114 | ) |>
115 | center_modal()
116 | }
117 |
118 | #' View account details UI
119 | #'
120 | #' @param ns Module namespace from which this function is called.
121 | #' @return [htmltools::tags$div()]
122 | #' @export
123 | view_account_ui <- \(ns) {
124 | tags$div(
125 | tags$h5("Edit account details"),
126 | reactableOutput(outputId = ns("account_details")),
127 | tags$div(
128 | id = ns("btn_container"),
129 | class = "d-none justify-content-between my-2",
130 | actionButton(
131 | inputId = ns("edit"),
132 | label = "Edit"
133 | )
134 | ),
135 | tags$hr(),
136 | tags$h5("Delete account"),
137 | actionButton(
138 | inputId = ns("delete"),
139 | label = "Delete"
140 | ),
141 | tags$hr(),
142 | actionButton(
143 | inputId = ns("go_back_to_dashboard"),
144 | label = "Go back"
145 | )
146 | )
147 | }
148 |
149 | #' Account UI module
150 | #'
151 | #' @param id String. Module id.
152 | #' @return [htmltools::tagList()]
153 | #' @export
154 | ui <- \(id) {
155 | ns <- NS(id)
156 |
157 | tagList(
158 | view_account_ui(ns = ns)
159 | )
160 | }
161 |
--------------------------------------------------------------------------------
/07_dynamic_rendering/store/contact.R:
--------------------------------------------------------------------------------
1 | box::use(
2 | htmltools[tags, tagList],
3 | . / nav[nav],
4 | . / create_card[create_card]
5 | )
6 |
7 | #' The "Contact" page
8 | #'
9 | #' @return An object of class `shiny.tag`
10 | #' @export
11 | contact <- \() {
12 | tagList(
13 | nav(active = "Contact"),
14 | tags$div(
15 | class = "container",
16 | create_card(
17 | title = "Talk to us",
18 | title_icon = tags$i(class = "bi bi-chat-dots"),
19 | title_class = "text-primary",
20 | class = "shadow-sm",
21 | tags$p(
22 | "Let us know what you think by sending us a message below"
23 | )
24 | ),
25 | create_card(
26 | class = "my-3 shadow-sm",
27 | contact_form()
28 | )
29 | )
30 | )
31 | }
32 |
33 | #' Contact form
34 | #'
35 | #' @export
36 | contact_form <- \() {
37 | tags$form(
38 | `hx-target` = "this",
39 | `hx-post` = "/contact",
40 | `hx-swap` = "outerHTML",
41 | email_input(),
42 | tags$div(
43 | class = "row",
44 | tags$div(
45 | class = "col-12 col-md-6",
46 | name_input(kind = "First")
47 | ),
48 | tags$div(
49 | class = "col-12 col-md-6",
50 | name_input(kind = "Last")
51 | )
52 | ),
53 | text_area_input(),
54 | submit_btn()
55 | )
56 | }
57 |
58 | #' Contact email input
59 | #'
60 | #' @param value Value of the input
61 | #' @param ... tags to append to end of div
62 | #' @param input_class Additional bootstrap classes for the input. Mostly used
63 | #' for validation.
64 | #' @return An object of class `shiny.tag`
65 | #' @export
66 | email_input <- \(..., value = "", input_class = NULL) {
67 | tags$div(
68 | class = "mb-3",
69 | `hx-target` = "this",
70 | `hx-swap` = "outerHTML",
71 | tags$label(
72 | `for` = "email",
73 | class = "form-label",
74 | "Email address"
75 | ),
76 | tags$input(
77 | type = "email",
78 | name = "email",
79 | id = "email",
80 | class = paste("form-control", input_class),
81 | required = NA,
82 | value = value,
83 | `hx-post` = "/contact/email"
84 | ),
85 | ...
86 | )
87 | }
88 |
89 | #' Name input
90 | #'
91 | #' @param kind String. Kind of name. Either "First", "Middle", or "Last".
92 | #' @param value String. Value of the input.
93 | #' @return An object of class `shiny.tag`
94 | #' @export
95 | name_input <- \(kind = "First", value = "") {
96 | id <- paste0(kind, "name")
97 | tags$div(
98 | class = "mb-3",
99 | tags$label(
100 | `for` = id,
101 | class = "form-label",
102 | paste(kind, "name")
103 | ),
104 | tags$input(
105 | type = "text",
106 | name = id,
107 | id = id,
108 | class = "form-control",
109 | value = value
110 | )
111 | )
112 | }
113 |
114 | #' Text area input
115 | #'
116 | #' @param ... tags to append to end of div
117 | #' @param id Input id
118 | #' @param label Input label
119 | #' @param value Input value
120 | #' @param input_class Additional bootstrap classes for the input. Mostly used
121 | #' for validation.
122 | #' @export
123 | text_area_input <- \(
124 | ...,
125 | id = "msg",
126 | label = "Your message",
127 | value = "",
128 | input_class = NULL
129 | ) {
130 | tags$div(
131 | class = "mb-3",
132 | `hx-target` = "this",
133 | `hx-swap` = "outerHTML",
134 | tags$label(
135 | `for` = id,
136 | class = "form-label",
137 | label
138 | ),
139 | tags$textarea(
140 | class = paste("form-control", input_class),
141 | id = id,
142 | name = id,
143 | rows = "3",
144 | required = NA,
145 | `hx-post` = "/contact/message",
146 | value
147 | ),
148 | ...
149 | )
150 | }
151 |
152 | #' Submit button
153 | #'
154 | #' @param class Button class
155 | #' @return An object of class `shiny.tag`
156 | #' @export
157 | submit_btn <- \(class = "btn btn-primary px-4 rounded-1") {
158 | tags$button(
159 | type = "submit",
160 | class = class,
161 | "Submit"
162 | )
163 | }
164 |
--------------------------------------------------------------------------------