diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000000..c4a8fee5514 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": "off", + "no-prototype-builtins": "off", + "no-useless-escape": "off", + "prefer-spread": "off" + } +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..b1f04ad75d3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# More details are here: https://help.github.com/articles/about-codeowners/ + +# The '*' pattern is global owners. + +# Order is important. The last matching pattern has the most precedence. +# The folders are ordered as follows: + +# In each subsection folders are ordered first by depth, then alphabetically. +# This should make it easy to add new rules without breaking existing ones. + +* @ionic-team/framework diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f22efb637ac..00000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,135 +0,0 @@ -# Contributing Guide - -Thanks for your interest in contributing to Ionic's documentation! :tada: Check the guidelines below for suggestions and requirements before submitting your contribution. - -* [Development Workflow](#development-workflow) -* [Project Structure](#project-structure) -* [Authoring Content](#authoring-content) -* [Reporting Issues](#reporting-issues) -* [Pull Request Guidelines](#pull-request-guidelines) -* [Project Management](#project-management) - ---- - -## Development Workflow - -In order to run the documentation locally, install the dependencies and run the development server: - -```sh -$ npm install -$ npm start -``` - -> **Note**: recent versions of npm (5+) and Node.js (8+) are required to run certain scripts. - ---- - -## Project Structure - -Ionic's documentation is built using [Stencil](https://stenciljs.com). The content is written as Markdown or pulled in as JSON data from other Ionic repositories. - -At a high level, the production documentation works like this: - -1. At build time, the `build-pages` script reads the Markdown in `src/pages/` and creates a JSON representation of each page at the same path - ``` - pages/ - ├── intro.json - └── intro.md - ``` -2. At runtime, the `docs-page` component receives the current path (e.g. `/docs/intro`) -3. The `docs-page` component fetches and parses the [JSON representation](https://ionicframework.com/docs/pages/intro.json) of that page -4. The `docs-page` component renders that data using a [template](../src/components/page/templates) - -> **Note**: most reference content (e.g. APIs, native plugins, CLI commands) is not stored as Markdown. Those pages are created using data provided by other repositories to the `build-pages` script. - -### Directories - -- `scripts/` - build scripts used to generate JSON representations of each page and other data used in the docs -- `src/` - source code and content of the docs - - `assets/` - static assets used on the site, like images and fonts - - `components/` - Stencil components used in the documentation UI - - `demos/` - Self-contained demos, optionally presented by pages via `demoUrl` YAML frontmatter - - `pages/` - Markdown content organized by route and uncommitted JSON representation of each page - - `styles/` - Global and page-specific styles (non-component styles) - ---- - -## Authoring Content - -The content of the Ionic docs is written as [Markdown](https://commonmark.org/) in `src/pages`. Each Markdown file corresponds to a route. - -``` -/docs/intro => src/pages/intro.md -/docs/installation/cli => src/pages/installation/cli.md -/docs/theming/advanced => src/pages/theming/advanced.md -/docs/theming => src/pages/theming.md -``` - -You can make copy edits to the site by [editing the Markdown files directly on GitHub](https://help.github.com/articles/editing-files-in-another-user-s-repository/). In your pull request, please explain what was missing from or inaccurate about the content. - -### Authoring Locally - -To edit or create content locally, you'll need to [run the development server](#development-workflow). By default, the pages are only built once while starting the server. You can rebuild the pages continuously as you edit them by concurrently running the `watch-pages` script: - -```sh -$ npm run watch-pages -``` - -> **Note**: the `watch-pages` script won't reload the page. You will need to reload the page manually after your page is rebuilt. - -### Reference Content - -The Markdown in `src/pages` does not contain all of the Ionic documentation's content: - -- Paths matching `/docs/api/*` are built from the [Ionic](https://github.com/ionic-team/ionic) source code -- Paths matching `/docs/native/*` are built from the [Ionic Native](https://github.com/ionic-team/ionic-native) source code -- Paths matching `/docs/cli/commands/*` are built from the [Ionic CLI](https://github.com/ionic-team/ionic-cli) source code - ---- - -## Reporting Issues - -Before submitting an issue to the Ionic docs repo, please search [existing issues](https://github.com/ionic-team/ionic-docs/issues) to avoid duplicate reports. - -If the issue you're reporting is a bug, please be sure it is an issue with the Ionic docs themselves and not the subject of the documentation. With your report, please provide: - -- Steps to reproduce -- Expected behavior -- OS and browser versions -- If possible, a demo repo or CodePen/CodeSandbox - ---- - -## Pull Request Guidelines - -When submitting pull requests, please keep the scope of your change contained to a single feature or bug. When in doubt, err on the side of smaller pull requests. If your pull request is a new feature, we would recommend opening an issue first to come to an agreement about the feature before putting in significant time. - -> **Note**: `tslint` will run automatically when you attempt to commit. Our lint rules extend [tslint-ionic-rules](https://github.com/ionic-team/tslint-ionic-rules). - ---- - -## Project Management - -Internally, the Ionic documentation team uses a [project board](https://github.com/ionic-team/ionic-docs/projects/3) to plan work on the docs. The lanes on the board are: - -- **Backlog** :file_cabinet: - Issues we plan to address, generally sorted by urgency -- **On Deck** :baseball: - Issues to be addressed during the current sprint, pulled from backlog during sprint planning -- **In Progress** :hammer: - Assigned issues that are currently being addressed -- **Needs Review** :mag: - Pull requests and issues that have a pending review -- **Done** :tada: - Issues that have been resolved - -If you're looking for issues to help out with, we'd recommend either asking about an issue in the backlog or checking for issues labeled [`help-wanted`](https://github.com/ionic-team/ionic-docs/labels/help%20wanted). - ---- - -## Deploying - -The Ionic documentation's `master` branch is deployed automatically and separately from the [Ionic site](https://github.com/ionic-team/ionic-site) itself. The Ionic site then uses a proxy for paths under `/docs` to request the deployed documentation. - ---- - -## License - -This repo is licensed and managed separately from Ionic itself. - -By contributing to this repo, you agree to have your contributions licensed under the Apache 2.0 license. See [LICENSE](../LICENSE) for the full license text. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 248d7fb9b3e..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Report a bug on the docs site -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Browser and OS (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..3cae620736e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,43 @@ +name: 🐛 Bug Report +description: Report a bug on the docs site +labels: ["triage"] +title: "bug: " +body: + - type: textarea + attributes: + label: Describe the Bug + description: A clear description of what the bug is and how it manifests. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Steps to Reproduce + description: Please explain the steps required to duplicate this issue. + validations: + required: true + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain the problem. + - type: input + attributes: + label: Operating System + description: The operating system you are running (e.g. Windows) + - type: input + attributes: + label: Browser + description: The browser you are running (e.g. Chrome, Safari) + - type: input + attributes: + label: Version + description: The browser version you are running (e.g. 14) + - type: textarea + attributes: + label: Additional Information + description: List any other information that is relevant to your issue. diff --git a/.github/ISSUE_TEMPLATE/content-issue.md b/.github/ISSUE_TEMPLATE/content-issue.md deleted file mode 100644 index 58b864b751c..00000000000 --- a/.github/ISSUE_TEMPLATE/content-issue.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Content issue -about: Report missing or inaccurate content on the docs -title: '' -labels: content -assignees: '' - ---- - -**URL** -The URL at which the content is missing or inaccurate - -**What is missing or inaccurate about the content on this page?** diff --git a/.github/ISSUE_TEMPLATE/content-issue.yml b/.github/ISSUE_TEMPLATE/content-issue.yml new file mode 100644 index 00000000000..404492e1736 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/content-issue.yml @@ -0,0 +1,17 @@ +name: 📕 Content Issue +description: Report missing or inaccurate content on the docs +labels: ["triage"] +title: "content: " +body: + - type: input + attributes: + label: URL + description: The URL at which the content is missing or inaccurate + validations: + required: true + - type: textarea + attributes: + label: Issue Description + description: What is missing or inaccurate about the content on this page? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index d513e5c0428..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest a feature for the docs -title: '' -labels: feature -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..5a964579982 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: 💡 Feature Request +description: Suggest a feature for the docs +labels: ["triage"] +title: "feat: " +body: + - type: textarea + attributes: + label: Describe Problem + description: A clear and concise description of what the problem is. Ex. I am always frustrated when [...] + validations: + required: true + - type: textarea + attributes: + label: Describe Preferred Solution + description: A clear and concise description of what you want to happen. + - type: textarea + attributes: + label: Describe Alternatives + description: A clear and concise description of any alternative solutions or features you have considered. + - type: textarea + attributes: + label: Additional Information + description: Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/ionic-issue-bot.yml b/.github/ionic-issue-bot.yml new file mode 100644 index 00000000000..f0da0d54073 --- /dev/null +++ b/.github/ionic-issue-bot.yml @@ -0,0 +1,86 @@ +triage: + label: triage + dryRun: false + +closeAndLock: + labels: + - label: 'ionitron: support' + message: > + Thanks for the issue! This issue appears to be a support request. We use this issue tracker exclusively for + content issues, bug reports and feature requests related to the documentation. Please use our + [forum](https://forum.ionicframework.com/) for help or questions about Ionic Framework. + + + Thank you for using Ionic! + - label: 'ionitron: missing template' + message: > + Thanks for the issue! It appears that you have not filled out the provided issue template. We use this issue + template in order to gather more information and further assist you. Please create a new issue and ensure the + template is fully filled out. + + + Thank you for using Ionic! + close: true + lock: true + dryRun: false + +comment: + labels: + - label: "help wanted" + message: > + This issue has been labeled as `help wanted`. This label is added to issues + that we believe would be good for contributors. + + + If you'd like to work on this issue, please comment here letting us know that + you would like to submit a pull request for it. This helps us to keep track of + the pull request and make sure there isn't duplicated effort. + + + For a guide on how to create a pull request and test this project locally to see + your changes, see our [contributing documentation](https://github.com/ionic-team/ionic-docs/blob/main/CONTRIBUTING.md). + + + Thank you! + - label: 'ionitron: needs reproduction' + message: > + Thanks for the issue! This issue has been labeled as `needs reproduction`. This label is added to issues that + we are not able to reproduce. + + + Please provide easy to follow steps for us to reproduce this issue. + dryRun: false + +noReply: + days: 14 + maxIssuesPerRun: 100 + label: "needs: reply" + responseLabel: triage + exemptProjects: true + exemptMilestones: true + message: > + Thanks for the issue! This issue is being closed due to the lack of a reply. If this is still an issue, + please create a new issue and ensure the template is fully filled out. + + + Thank you for using Ionic! + close: true + lock: true + dryRun: false + +noReproduction: + days: 14 + maxIssuesPerRun: 100 + label: "ionitron: needs reproduction" + responseLabel: triage + exemptProjects: true + exemptMilestones: true + message: > + Thanks for the issue! This issue is being closed due to the lack of a reproduction. If this is still an issue, + please create a new issue and ensure the template is fully filled out. + + + Thank you for using Ionic! + close: true + lock: true + dryRun: false diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index 6ffe781d3ad..00000000000 --- a/.github/main.workflow +++ /dev/null @@ -1,21 +0,0 @@ -workflow "Build and Test" { - on = "push" - resolves = ["Build"] -} - -action "Install" { - uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" - args = "install" -} - -action "Lint" { - uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" - needs = ["Install"] - args = "run lint" -} - -action "Build" { - uses = "actions/npm@de7a3705a9510ee12702e124482fad6af249991b" - needs = ["Lint"] - args = "run build" -} diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000000..66c35d1a18e --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,42 @@ +# GitHub Actions docs +# https://help.github.com/en/articles/about-github-actions +# https://help.github.com/en/articles/workflow-syntax-for-github-actions + +name: Validate Docs + +on: [pull_request] + +jobs: + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + node_version: [20] + # Build processes work differently across operating systems, so the + # checks run on each one to make sure anyone can contribute to the + # docs. + os: [windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: ⚙️ Use Node.js ${{ matrix.node_version }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ matrix.node_version }} + - name: 🕸️ Install Dependencies + run: npm ci --legacy-peer-deps + - name: 🖌️ Lint + run: npm run lint + - name: Test + run: npm run test + - name: 🔤 Spell Check + run: npm run spellcheck + - uses: ./.github/workflows/actions/check-translations + # Lint and spell check changes should be pushed + # to the branch before the branch is merge eligible. + # + # The translation keys check should not produce any changes. + - name: 🔍 Check Diff + run: git diff --exit-code + shell: bash diff --git a/.github/workflows/actions/check-translations/action.yml b/.github/workflows/actions/check-translations/action.yml new file mode 100644 index 00000000000..3e53a643597 --- /dev/null +++ b/.github/workflows/actions/check-translations/action.yml @@ -0,0 +1,20 @@ +name: 'Check Translations' +description: 'Validates the translated locales without building them' +runs: + using: 'composite' + steps: + # Surfaces failures unique to a translated locale, such as duplicate + # translation keys, while the changes are still in review. Previews only + # build the default locale, so translated locale issues would not surface + # until after merging into main. + # + # The sidebars reference the native and CLI docs, which are generated rather + # than committed, so those have to exist before the translations can be + # extracted. + - name: 🇯🇵 Extract Translations + shell: bash + run: | + trap 'rm -rf i18n' EXIT + node scripts/native.mjs + node scripts/cli.mjs + npx docusaurus write-translations --locale ja diff --git a/.gitignore b/.gitignore index f1e6917a402..a1ae05b6ea1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,25 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader +src/components/page/reference/ReleaseNotes/release-notes.json + +# Misc .DS_Store -node_modules/ -.stencil/ -www/ -src/pages/**/*.json -src/components/menu/data/ -src/components/page/data/ -src/components/search/data/ +.env.local +.env.development.local +.env.test.local +.env.production.local +.env + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +static/**/node_modules/ +.idea diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..4fc33ed30b4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,36 @@ +# Ejected components keep upstream's formatting so they stay diffable against +# @docusaurus/theme-classic. Refer to `src/theme/README.md`. +# Wrapped components are our own code, so they follow the repo's formatting. +src/theme/DocItem/Layout/index.tsx +src/theme/EditMetaRow/index.tsx +src/theme/Icon/Edit/index.tsx +src/theme/Icon/Language/index.tsx +src/theme/Layout/index.tsx +src/theme/NavbarItem/LocaleDropdownNavbarItem/index.tsx +src/theme/prism-include-languages.ts + +legacy-stencil-components +scripts/bak + +docs/api +versioned_docs/version-v*/api +docs/native +versioned_docs/version-v*/native +docs/cli/commands +# Each definition in these files is one line of prose inside a JSX
. +# Prettier's mdx parser reflows those children and moves link text onto its own +# line, which MDX then wraps in a paragraph, rendering invalid HTML such as +#

Android SDK

. Formatting these files reintroduces that markup. +docs/reference/glossary.md +versioned_docs/version-v*/reference/glossary.md + +# Archived versions +versioned_docs/version-v5 +versioned_docs/version-v6 + +static/code/stackblitz + +.docusaurus +.github +build +node_modules diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..e50b8ef32ea --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Prerender", + "args": [ + "${workspaceFolder}/node_modules/.bin/stencil", + "build", + "--debug", + "--prerender", + "--max-workers=1" + ], + "protocol": "inspector" + } + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..534db3dd80a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,248 @@ +# Contributing Guide + +Thanks for your interest in contributing to Ionic's documentation! :tada: Check the guidelines below for suggestions and requirements before submitting your contribution. + + + TABLE OF CONTENTS + + +- [Development Workflow](#development-workflow) + - [Previewing Changes](#previewing-changes) + - [Linting Documentation](#linting-documentation) + - [Spell Check](#spell-check) +- [Using VS Code on Windows](#using-vs-code-on-windows) +- [Project Structure](#project-structure) + - [Directories](#directories) +- [Authoring Content](#authoring-content) + - [Reference Content](#reference-content) +- [Translation](#translation) +- [Reporting Issues](#reporting-issues) +- [Pull Request Guidelines](#pull-request-guidelines) +- [Deploying](#deploying) + - [Archiving a Version](#archiving-a-version) +- [License](#license) + +--- + +## Development Workflow + +### Previewing Changes + +In order to run the documentation locally, install the dependencies and run the development server: + +```sh +npm install +npm start +``` + +#### GitHub Token + +The documentation build requires a GitHub Personal Access Token to fetch Ionic Framework release notes. The build will still work locally without it (release notes will be empty), but it's required for Vercel preview and production builds. + +**Local Development:** + +1. Create a [fine-grained Personal Access Token](https://github.com/settings/personal-access-tokens/new) with: + + - **Repository access**: Public repositories (read-only) + - **Expiration**: 366 days (update annually) + +2. Add the token to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.): + + ```sh + export GITHUB_TOKEN=github_pat_... + ``` + +3. Reload your shell or run `source ~/.zshrc` (or equivalent) + +**Vercel:** + +1. Create a [fine-grained Personal Access Token](https://github.com/settings/personal-access-tokens/new) with the same settings as above, but with: + + - **Owner**: ionic-team organization (not your personal account) + +2. Add the token to Vercel project settings: + + - Go to your project on Vercel + - Navigate to **Settings → Environment Variables** + - Add `GITHUB_TOKEN` with the token value + - Select Production and Preview environments + +3. Redeploy the project for the token to take effect + +Without the token, the build will fail with an error message indicating the token is missing. + +### Linting Documentation + +This repository uses [Prettier](https://prettier.io/), an opinionated code formatter, in order to keep consistent formatting throughout the documentation. Run the following command to automatically fix all formatting, and then push any changes: + +``` +npm run lint +``` + +### Spell Check + +This repository uses [cspell](https://cspell.org/), a spell checker for code, to automatically flag any spelling errors. Run the following command to see any spelling errors: + +``` +npm run spellcheck +``` + +> [!NOTE] +> Any spelling errors will need to be fixed manually. There are various ways to ignore words or sections that were flagged erroneously. These are listed below. + +#### Ignoring words + +**To ignore:** + +- A **specific word**, add it to the following file: `cspell-wordlist.txt` + - For example, `Ionicons` is flagged as an unknown word. Since this is the name of our software, it has been added to this file to be ignored. +- A **directory** or anything matching a **regular expression**, update the following file: `cspell.json` + - For example, we don't want to flag anything inside of code ticks (`) or code blocks (```), so there are regular expressions added to ignore anything inside of these. +- An **entire line**, add the following comment above it: + ```markdown + + ``` +- **Multiple lines**, add comments above and below the lines to be ignored: + + ```markdown + + +

Everything inside of these comments will be ignored by the spell checkr. Proofread your own words carefully.

+ + + ``` + +> [!IMPORTANT] +> You need to have line breaks between the `cspell` comments and any HTML elements, +> otherwise the build will error with `Module build failed`. + +#### Tips + +Before adding a word or section to be ignored, see if there is a way to make it pass the spell check. Technical terms that are part of an API may need to be wrapped in code formatting. For example, the word `keydown` is flagged as an unknown word by the spell checker, but this is a [Web API event](https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event). We can wrap any mentions of `keydown` in two backticks (\`keydown\`) in order to avoid it being flagged by the spell checker. + +Comments disabling the next line or entire sections of documentation are useful for making the spell checker ignore people's names. + +In general, we should try to avoid ignoring words unless they are technical terms that are used throughout the documentation and wouldn't necessarily make sense formatted as code. + +--- + +## Using VS Code on Windows + +The Ionic docs were originally built in a Mac-based environment, so Mac-focused linting rules apply when committing changes. To contribute on Windows, do the following: + +- Configure VS Code to read/save files using line breaks (LF) instead of carriage returns (CRLF). Set it globally by navigating to: Settings -> Text Editor -> Files -> Eol. Set to `\n`. +- Check that the Git setting `core.autocrlf` is set to `false`: run `git config -l | grep autocrlf`. Switch it to false using: `git config --global core.autocrlf false`. +- If you've already cloned the `ionic-docs` repo, the files may already be cached as LF. To undo this, you need to clean the cache files of the repository. Run the following (make sure you stage or commit your changes first): `git rm --cached -r .` then `git reset --hard`. + +## Project Structure + +Ionic's documentation is built using [Docusaurus](https://docusaurus.io/). The content is either written or generated as Markdown. + +### Directories + +- `scripts/` - build scripts used to generate markdown or json files +- `src/` - source code and content of the docs + - `components/` - components used across the site + - `global/` - components used globally + - `page/` - components used on a single page or in a limited scope + - `styles/` - global styles and variables + - `components/` - styles split out into the components they target +- `static/` + - `demos/` - self-contained demos, optionally presented by pages via `demoUrl` YAML frontmatter + - `usage/` - playgrounds that can be created by running `npm run playground:new` [(docs)](_templates/README.md#new-playground-template) +- `versioned_docs/` - versions of the docs created by the docusaurus versioning command +- `versioned_sidebars/` - versions of the docs sidebars created by the docusaurus versioning command + +## Authoring Content + +The content of the Ionic docs is written as [Markdown](https://commonmark.org/) in `docs/`. Each Markdown file corresponds to a route unless explicitly changed in the frontmatter. + +``` +/docs/ => src/pages/index.md +/docs/intro/cli => src/pages/intro/cli.md +/docs/theming/advanced => src/pages/theming/advanced.md +/docs/theming => src/pages/theming.md +``` + +You can make copy edits to the site by [editing the Markdown files directly on GitHub](https://help.github.com/articles/editing-files-in-another-user-s-repository/). In your pull request, please explain what was missing from or inaccurate about the content. + +### Reference Content + +The Markdown in `docs/` does not only contain manually written markdown files: + +- Paths matching `/docs/api/*` are built from the [Ionic Framework](https://github.com/ionic-team/ionic) source code +- Paths matching `/docs/native/*` are built from the [Ionic Native](https://github.com/ionic-team/ionic-native) source code +- Paths matching `/docs/cli/commands/*` are built from the [Ionic CLI](https://github.com/ionic-team/ionic-cli) source code + +## Translation + +The Ionic docs have been translated into Japanese and are in the process of being translated into Chinese, French, Portuguese, and Spanish. We've chosen these languages because we believe they have the greatest number of developers where English-only documentation would be a barrier. + +We use Crowdin for our translation service. You can participate in the translation effort on the [Ionic Crowdin page](https://crowdin.com/project/ionic-docs). + +_Please submit translation issues to the Crowdin page and not the Ionic Docs GitHub repo._ + + + +The Japanese translation of the docs were built by an independent team, lead by [rdlabo](https://github.com/rdlabo) and can be found and contributed to on the [ionic-jp group's `ionic-docs` project page](https://github.com/ionic-jp/ionic-docs). + +## Reporting Issues + +Before submitting an issue to the Ionic docs repo, please search [existing issues](https://github.com/ionic-team/ionic-docs/issues) to avoid duplicate reports. + +If the issue you're reporting is a bug, please be sure it is an issue with the Ionic docs themselves and not the subject of the documentation. With your report, please provide: + +- Steps to reproduce +- Expected behavior +- OS and browser versions +- If possible, a demo repo or CodePen/CodeSandbox + +> [!NOTE] +> Some [reference content](#reference-content) is pulled from other Ionic repos. In that case, please submit your issue on the docs repo with a link to the repo where the content lives. + +--- + +## Pull Request Guidelines + +When submitting pull requests, please keep the scope of your change contained to a single feature or bug. When in doubt, err on the side of smaller pull requests. If your pull request is a new feature, we would recommend opening an issue first to come to an agreement about the feature before putting in significant time. + +--- + +## Deploying + +The Ionic documentation's `main` branch is deployed automatically and separately from the [Ionic site](https://github.com/ionic-team/ionic-site) itself. The Ionic site then uses a proxy for paths under `/docs` to request the deployed documentation. + +### Archiving a Version + +Archived versions are served from a frozen Vercel deployment instead of being rebuilt on every `main` deploy, which keeps build times and memory usage low. Two files control this: + +- [`versions.json`](./versions.json): lists the versions Docusaurus rebuilds on every deploy. +- [`versionsArchived.json`](./versionsArchived.json): maps each archived version to the frozen deployment URL the version picker links to. + +The archived URL has to point at a build that _included_ the version, so you build it first, then move it to `versionsArchived.json`: + +1. **Build the version.** Make sure it is in `versions.json`. If you are refreshing an already-archived version, move it out of `versionsArchived.json` and back into `versions.json`. Commit, push and let Vercel deploy. +2. **Promote the deployment.** In the Vercel dashboard, open that deployment and **Promote to Production** so it does not get cleaned up. Wait for the build to finish before pushing again, or it may get canceled. +3. **Copy its URL.** Use the deployment's unique `ionic-docs--ionic1.vercel.app` URL, not the branch or production alias. +4. **Archive it.** Remove the version from `versions.json`, then add it to `versionsArchived.json` with `/docs/` appended and no trailing slash (a trailing slash causes a brief 404 flash): + + ```json + { + "v6": "https://ionic-docs--ionic1.vercel.app/docs/v6" + } + ``` + +5. **Open a PR.** Once merged, the version picker links to the archive and `main` stops building that version. + +Removed versions keep their `versioned_docs/` and `versioned_sidebars/` content, so they can be rebuilt anytime by adding them back to `versions.json`. + +> [!NOTE] +> Ionic v3 and v4 use other build tools and are not managed here. + +--- + +## License + +This repo is licensed and managed separately from Ionic itself. + +By contributing to this repo, you agree to have your contributions licensed under the Apache 2.0 license. See [LICENSE](LICENSE) for the full license text. diff --git a/Procfile b/Procfile deleted file mode 100644 index 489b2700aca..00000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: node server.js diff --git a/README.md b/README.md index ff2098568a1..a82f4da1ca6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Ionic Docs -The official [Ionic](https://ionicframework.com) documentation, built with [Stencil](https://stenciljs.com). +The official [Ionic](https://ionicframework.com) documentation, built with [Docusaurus](https://docusaurus.io/). + +[![Crowdin](https://badges.crowdin.net/ionic-docs/localized.svg)](https://crowdin.com/project/ionic-docs) --- -- [Contributing Guide](https://github.com/ionic-team/ionic-docs/blob/master/.github/CONTRIBUTING.md) :flashlight: -- [Project Board](https://github.com/ionic-team/ionic-docs/projects/3) :pushpin: +- [Contributing Guide](./CONTRIBUTING.md) :flashlight: + diff --git a/_templates/README.md b/_templates/README.md new file mode 100644 index 00000000000..7f5e1fe828a --- /dev/null +++ b/_templates/README.md @@ -0,0 +1,32 @@ +# Hygen templates + +The templates in this directory are intended to be used with [hygen](https://www.hygen.io/) to generate boilerplate files. Check out [the root package.json](../package.json) to see if there are any custom commands to use them (e.g. `npm run playground:new`). You can also run e.g. `hygen playground new` to use a generator. + +Some helpful docs links for updating/creating templates: + +- [enquirer](https://github.com/enquirer/enquirer#toggle-prompt) for building command line prompts +- [inflection](https://www.hygen.io/docs/templates#helpers-and-inflections) and [change case](https://www.hygen.io/docs/templates#change-case-helpers) for e.g. changing the case of variables submitted via the prompts + +# New playground template + +## Generation + +To create a new playground, run `npm run playground:new`. This will walk you through some prompts to decide what files for the generator to create for the playground, and what their paths should be. + +The path defaults to `basic`. If there is already a basic playground, you'll want to input a different path for the playground. + +The CSS option will add extra files if you need to include custom CSS in your playground. + +If you need a component for multiple versions of Ionic Framework, you (currently) need to run the generator once for each version. + +## Usage + +Once you've generated your playground, you need to add it to the main markdown file in the docs (e.g. [docs/api/button.md](../docs/api/button.md)) by doing something similar to the following example: + +``` +## Feature + +import Feature from '@site/static/usage/v9/button/feature/index.md'; + + +``` diff --git a/_templates/playground/new/angular_example_component_css.md.ejs.t b/_templates/playground/new/angular_example_component_css.md.ejs.t new file mode 100644 index 00000000000..e6aab594e42 --- /dev/null +++ b/_templates/playground/new/angular_example_component_css.md.ejs.t @@ -0,0 +1,9 @@ +--- +# this file only gets generated if `css` (from the command line prompt) is true +to: "<%= css ? `static/usage/v${version}/${name}/${path}/angular/example_component_css.md` : null %>" +--- +```css +<%= component %> { + /* styles go here */ +} +``` diff --git a/_templates/playground/new/angular_example_component_html.md.ejs.t b/_templates/playground/new/angular_example_component_html.md.ejs.t new file mode 100644 index 00000000000..98f48fde212 --- /dev/null +++ b/_templates/playground/new/angular_example_component_html.md.ejs.t @@ -0,0 +1,6 @@ +--- +to: "<%= `static/usage/v${version}/${name}/${path}/angular/example_component_html.md` %>" +--- +```html +<<%= component %>>> +``` diff --git a/_templates/playground/new/angular_example_component_ts.md.ejs.t b/_templates/playground/new/angular_example_component_ts.md.ejs.t new file mode 100644 index 00000000000..348bbb0b8df --- /dev/null +++ b/_templates/playground/new/angular_example_component_ts.md.ejs.t @@ -0,0 +1,16 @@ +--- +arbitrary: <% pascalComponent = h.changeCase.pascal(component) %> +to: "<%= `static/usage/v${version}/${name}/${path}/angular/example_component_ts.md` %>" +--- +```ts +import { Component } from '@angular/core'; +import { <%= pascalComponent %> } from '@ionic/angular'; + +@Component({ + selector: 'app-example', + templateUrl: 'example.component.html', + styleUrls: ['./example.component.css'], + imports: [<%= pascalComponent %>], +}) +export class ExampleComponent {} +``` diff --git a/_templates/playground/new/demo.html.ejs.t b/_templates/playground/new/demo.html.ejs.t new file mode 100644 index 00000000000..3a6a9cadff9 --- /dev/null +++ b/_templates/playground/new/demo.html.ejs.t @@ -0,0 +1,32 @@ +--- +arbitrary: <% numberOfAncestors = (path.match(/\//g) || []).length; directoryChanges = '../'.repeat(numberOfAncestors) %> +to: "<%= `static/usage/v${version}/${name}/${path}/demo.html` %>" +--- + + + + + + <%= h.changeCase.titleCase(name) %> + + + + <% if (css){ %> + + <% } %> + + + + + +
+ <<%= component %>>> +
+
+
+ + diff --git a/_templates/playground/new/index.js b/_templates/playground/new/index.js new file mode 100644 index 00000000000..c69d04db737 --- /dev/null +++ b/_templates/playground/new/index.js @@ -0,0 +1,82 @@ +const changeCase = require('change-case'); + +// see types of prompts: +// https://github.com/enquirer/enquirer/tree/master/examples +// +module.exports = { + prompt: ({ inquirer }) => { + return inquirer + .prompt([ + { + type: 'toggle', + name: 'is_component', + message: 'Is this playground for a component?', + initial: true, + }, + ]) + .then((answers) => { + return inquirer + .prompt([ + // ask a different question for components vs. other playgrounds + answers.is_component + ? { + type: 'input', + name: 'component', + message: 'Which component is this playground for?', + initial: 'ion-button', + validate(value) { + return value.match(/^ion-[a-z/-]*[a-z]+$/) + ? true + : "Component name must be kebab-case and begin with 'ion-'"; + }, + } + : { + type: 'input', + name: 'name', + message: 'Which guide section is this playground for?', + initial: 'animations', + validate(value) { + return value.match(/^[a-z/-]+$/) ? true : 'Section must be kebab-case'; + }, + }, + { + type: 'input', + name: 'path', + message: 'What should the playground path be?', + hint: 'e.g. `basic` or `theming/colors`', + validate(value) { + return value.match(/^[a-z]+[a-z/-]*[a-z]+$/) + ? true + : "Path should begin and end with a letter and only contain lowercase letters, '-', or '/'"; + }, + }, + { + type: 'select', + name: 'version', + message: 'Select the Ionic Framework version for the playground', + initial: '9', + choices: ['6', '7', '8', '9'], + }, + { + type: 'toggle', + name: 'css', + message: 'Generate custom CSS files?', + }, + ]) + .then((answers) => { + answers.name = answers.name || answers.component.replace('ion-', ''); + + // if the playground is not for a component, + // include an ion-card in the playground + answers.component = answers.component || 'ion-card'; + + const playgroundName = changeCase.pascal(answers.path.split('/').pop()); + console.log( + `\nTo use this playground in a docs markdown file, include\nthe following:\n\n## ${playgroundName}\n\nimport ${playgroundName} from '@site/static/usage/v${answers.version}/${answers.name}/${answers.path}/index.md';\n\n<${playgroundName} />\n` + ); + + return answers; + }); + }); + }, +}; diff --git a/_templates/playground/new/index.md.ejs.t b/_templates/playground/new/index.md.ejs.t new file mode 100644 index 00000000000..148d3e85efe --- /dev/null +++ b/_templates/playground/new/index.md.ejs.t @@ -0,0 +1,48 @@ +--- +to: "<%= `static/usage/v${version}/${name}/${path}/index.md` %>" +--- +import Playground from '@site/src/components/global/Playground'; + +import javascript from './javascript.md'; +<% if (css){ %> +import react_main_tsx from './react/main_tsx.md'; +import react_main_css from './react/main_css.md'; + +<% } else { -%> +import react from './react.md'; +<% } -%> +import vue from './vue.md'; + +import angular_example_component_html from './angular/example_component_html.md'; +<% if (css){ -%> +import angular_example_component_css from './angular/example_component_css.md'; +<% } -%> +import angular_example_component_ts from './angular/example_component_ts.md'; + + + react: { + files: { + 'src/main.tsx': react_main_tsx, + 'src/main.css': react_main_css, + }, + }, +<% } else { -%> + react, +<% } -%> + vue, + angular: { + files: { + 'src/app/example.component.html': angular_example_component_html, +<% if (css){ -%> + 'src/app/example.component.css': angular_example_component_css, +<% } -%> + 'src/app/example.component.ts': angular_example_component_ts, + }, + }, + }} + src="usage/v<%= version %>/<%= name %>/<%= path %>/demo.html" +/> diff --git a/_templates/playground/new/javascript.md.ejs.t b/_templates/playground/new/javascript.md.ejs.t new file mode 100644 index 00000000000..1aa3e7f1f2e --- /dev/null +++ b/_templates/playground/new/javascript.md.ejs.t @@ -0,0 +1,14 @@ +--- +to: "<%= `static/usage/v${version}/${name}/${path}/javascript.md` %>" +--- +```html +<<%= component %>>> +<% if (css){ -%> + + +<% } -%> +``` diff --git a/_templates/playground/new/react.md.ejs.t b/_templates/playground/new/react.md.ejs.t new file mode 100644 index 00000000000..a8320a026ba --- /dev/null +++ b/_templates/playground/new/react.md.ejs.t @@ -0,0 +1,20 @@ +--- +arbitrary: <% pascalComponent = h.changeCase.pascal(component) %> +# this file's location depends on whether or not the css option is selected via the prompt +to: "<%= `static/usage/v${version}/${name}/${path}/${css ? 'react/main_tsx.md' : 'react.md'}` %>" +--- +```tsx +import React from 'react'; +import { <%= pascalComponent %> } from '@ionic/react';<% if (css){ %> + +import './main.css';<% } %> + +function Example() { + return ( + <> + <<%= pascalComponent %>>> + + ); +} +export default Example; +``` diff --git a/_templates/playground/new/react_main_css.md.ejs.t b/_templates/playground/new/react_main_css.md.ejs.t new file mode 100644 index 00000000000..1abbc16100c --- /dev/null +++ b/_templates/playground/new/react_main_css.md.ejs.t @@ -0,0 +1,9 @@ +--- +# this file only gets generated if `css` (from the command line prompt) is true +to: "<%= css ? `static/usage/v${version}/${name}/${path}/react/main_css.md` : null %>" +--- +```css +<%= component %> { + /* styles go here */ +} +``` diff --git a/_templates/playground/new/vue.md.ejs.t b/_templates/playground/new/vue.md.ejs.t new file mode 100644 index 00000000000..5867eb38b95 --- /dev/null +++ b/_templates/playground/new/vue.md.ejs.t @@ -0,0 +1,21 @@ +--- +arbitrary: <% pascalComponent = h.changeCase.pascal(component) %> +to: "<%= `static/usage/v${version}/${name}/${path}/vue.md` %>" +--- +```vue + + + +<% if (css){ -%> + + +<% } -%> +``` diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000000..c237cf6e3bd --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,12 @@ +project_id: '359747' +api_token_env: 'CROWDIN_PERSONAL_TOKEN' +preserve_hierarchy: true + +files: + - source: /i18n/en/**/* + translation: /i18n/%two_letters_code%/**/%original_file_name% + - source: /docs/**/* + translation: /i18n/%two_letters_code%/docusaurus-plugin-content-docs/current/**/%original_file_name% + ignore: + - /docs/cli/commands/**/* + - /docs/native/**/* diff --git a/cspell-wordlist.txt b/cspell-wordlist.txt new file mode 100644 index 00000000000..13b3252d864 --- /dev/null +++ b/cspell-wordlist.txt @@ -0,0 +1,92 @@ +Appflow +Codepen +Firestore +Genymotion +Hygen +Ionicon +Ionicons +Logcat +Maskito +Pluralsight +Swiper +Udemy +Vetur +Wistia +WCAG +CDK + +actionsheet +fabs +datetime +datetimes +datetimepicker +infinitescroll +searchbar +searchbars +sidemenu +textarea +textareas + +appstore +authed +autogrow +automations +autoplay +Callout +codemods +comparewith +composables +engageable +flexbox +fontawesome +fortawesome +frontmatter +fullscreen +geolocation +iconset +interactives +isopen +jank +janky +jarsigner +jsdelivr +keyframes +keytool +lifecycles +localstorage +mobileweb +phablet +playstore +preconfigured +preflighted +proxying +quickstart +remixicon +retargeted +runtimes +scroller +subcomponent +subcomponents +subpath +subpaths +svgs +swipeable +templating +transpiling +treeshaking +triaging +typeahead +unminified +unsanitized +viewports +webapps +webviews +xlarge +xsmall + +allowtransparency +mozallowfullscreen +msallowfullscreen +oallowfullscreen +webkitallowfullscreen +webnative diff --git a/cspell.json b/cspell.json new file mode 100644 index 00000000000..57eed251d20 --- /dev/null +++ b/cspell.json @@ -0,0 +1,28 @@ +{ + "dictionaries": ["custom-words"], + "dictionaryDefinitions": [ + { + "name": "custom-words", + "path": "./cspell-wordlist.txt", + "addWords": true + } + ], + "ignoreRegExpList": [ + "/(```+)[\\s\\S]+?\\1/g", + "`([^`]*)`", + "/:[a-zA-Z0-9-_\\+]+:/g" + ], + "ignorePaths": [ + "docs/cli", + "docs/native", + "versioned_docs/**/api", + "versioned_docs/**/cli", + "versioned_docs/**/native", + "versioned_docs/version-v5", + "versioned_docs/version-v6", + "node_modules" + ], + "flagWords": [ + "hte" + ] +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..12005305278 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# Docs folder + +The `/docs` folder houses all markdown files. The page structure loosely maps to the routing on the site since paths can be changed in the frontmatter. + +## Versioning + +This folder can also contain components, assets, and whatever else is meant to be versioned when the docusaurus versioning script is run. For example, if there is a page component that is only relevant to the `layout` section in the current version of Ionic, it could be added to a `_components/` folder in `docs/layout/`. When the versioning script is run, the component will be copied to `versioned_docs/version-{X}/layout/_components/` and there will now be a separate component in `docs/layout/_components/` that can be deleted or updated to the latest version. The same concept applies to images and other files. + +If components are meant to be shared across versions, they can be put in `src/components/`. If images and other served files are meant to be shared across versions they can be put in `static/`. + +## Auto Generated Files + +All markdown files in these directories are generated from [scripts](/scripts): + +- `docs/api/` +- `docs/cli/commands/` +- `docs/native/` diff --git a/docs/angular/add-to-existing.md b/docs/angular/add-to-existing.md new file mode 100644 index 00000000000..8a551abfd4e --- /dev/null +++ b/docs/angular/add-to-existing.md @@ -0,0 +1,341 @@ +--- +title: Add to Existing Angular Project +sidebar_label: Add to Existing +--- + +import DocsCard from '@components/global/DocsCard'; +import DocsCards from '@components/global/DocsCards'; + + + Add Ionic Angular to Existing Project: Integration Guide + + + +This guide covers how to add Ionic Angular to an existing Angular project. If you're looking to start a new project from scratch, check out the [Ionic Angular Quickstart](/docs/angular/quickstart.md) guide. For an overview of how Ionic Angular works with Angular, including version support and tooling, check out the [Ionic Angular Overview](/docs/angular/overview.md). + +:::tip + +This guide uses `.css` file extensions for stylesheets. If you created your Angular app with a different stylesheet format (such as `.scss`, `.sass`, or `.less`), use that extension instead. + +::: + +## Setup + +:::info + +This guide follows the structure of an Angular app created with the Angular CLI. If you started your Angular app using a different method, your file structure and setup may differ. + +::: + +You can add Ionic Angular to your existing Angular project using the Angular CLI's `ng add` feature or by installing it manually. + +### Using ng add + +The easiest way to add Ionic Angular is to use the Angular CLI's `ng add` feature: + +```bash +ng add @ionic/angular +``` + +This will install the `@ionic/angular` package and automatically configure the necessary imports and styles. + +### Manual Installation + +If you prefer to install Ionic Angular manually, you can follow these steps: + +#### 1. Install the Package + +```bash +npm install @ionic/angular +``` + +#### 2. Add Ionic Framework Stylesheets + +Replace the existing `styles` array in `angular.json` with the following: + +```json title="angular.json" +"styles": [ + "src/styles.css", + { + "input": "node_modules/@ionic/angular/css/core.css" + }, + { + "input": "node_modules/@ionic/angular/css/normalize.css" + }, + { + "input": "node_modules/@ionic/angular/css/structure.css" + }, + { + "input": "node_modules/@ionic/angular/css/typography.css" + } +] +``` + +:::info + +While `core.css` is required, `normalize.css`, `structure.css`, and `typography.css` are recommended but not required. They normalize cross-browser differences, ensure proper scrolling behavior, and provide consistent typography and form styling. Without them, you may need to handle these concerns yourself. For more details, refer to [Global Stylesheets](/docs/layout/global-stylesheets.md). + +::: + +#### 3. Configure Ionic Angular + +Update `src/app/app.config.ts` to include `provideIonicAngular`: + +```typescript title="src/app/app.config.ts" +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { provideIonicAngular } from '@ionic/angular'; + +export const appConfig: ApplicationConfig = { + providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes), provideIonicAngular({})], +}; +``` + +This reflects the Angular 21 and 22 scaffold, which is zoneless by default. If your existing app is on Angular 18 through 20, it still has `provideZoneChangeDetection({ eventCoalescing: true })`; keep that provider and add `provideIonicAngular({})` alongside it. Refer to [Zoneless Change Detection](/docs/angular/zoneless.md) for details. + +## Using Individual Components + +After completing the setup above, you can start using Ionic components in your existing Angular app. Here's an example of how to use them: + +Update `src/app/app.html` to the following: + +```html title="src/app/app.html" +Button +``` + +Then, import the components in `src/app/app.ts`: + +```ts title="src/app/app.ts" +import { Component } from '@angular/core'; +import { IonButton, IonDatetime } from '@ionic/angular'; + +@Component({ + selector: 'app-root', + imports: [IonButton, IonDatetime], + templateUrl: './app.html', + styleUrl: './app.css', +}) +export class App {} +``` + +Visit the [components](/docs/components.md) page for all of the available Ionic components. + +## Using Ionic Pages + +If you want to use Ionic pages with full navigation and page transitions, follow these additional setup steps. + +#### 1. Add Additional Ionic Framework Stylesheets + +Replace the existing `styles` array in `angular.json` with the following: + +```json title="angular.json" +"styles": [ + "src/styles.css", + { + "input": "node_modules/@ionic/angular/css/core.css" + }, + { + "input": "node_modules/@ionic/angular/css/normalize.css" + }, + { + "input": "node_modules/@ionic/angular/css/structure.css" + }, + { + "input": "node_modules/@ionic/angular/css/typography.css" + }, + { + "input": "node_modules/@ionic/angular/css/display.css" + }, + { + "input": "node_modules/@ionic/angular/css/padding.css" + }, + { + "input": "node_modules/@ionic/angular/css/float-elements.css" + }, + { + "input": "node_modules/@ionic/angular/css/text-alignment.css" + }, + { + "input": "node_modules/@ionic/angular/css/text-transformation.css" + }, + { + "input": "node_modules/@ionic/angular/css/flex-utils.css" + }, + { + "input": "src/theme/variables.css" + } +] +``` + +These stylesheets set up the overall page structure and provide [CSS utilities](/docs/layout/css-utilities.md) for faster development. Some stylesheets are optional. For details on which stylesheets are required, check out [Global Stylesheets](/docs/layout/global-stylesheets.md). + +#### 2. Set up Theming + +Create a `src/theme/variables.css` file with the following content: + +```css title="src/theme/variables.css" +/** + * Ionic Dark Theme + * ----------------------------------------------------- + * For more info, please refer to: + * https://ionicframework.com/docs/theming/dark-mode + */ + +/* @import "@ionic/angular/css/palettes/dark.always.css"; */ +/* @import "@ionic/angular/css/palettes/dark.class.css"; */ +@import '@ionic/angular/css/palettes/dark.system.css'; +``` + +This file enables [dark mode support](/docs/theming/dark-mode.md) for your Ionic app when the system is set to prefer a dark appearance. You can customize the theming behavior by uncommenting different dark palette imports or adding custom CSS variables. + +#### 3. Update the App Component + +Update `src/app/app.html` to the following: + +```html title="src/app/app.html" + + + +``` + +Then, update `src/app/app.ts` to include the component imports: + +```ts title="src/app/app.ts" +import { Component } from '@angular/core'; +import { IonApp, IonRouterOutlet } from '@ionic/angular'; + +@Component({ + selector: 'app-root', + imports: [IonApp, IonRouterOutlet], + templateUrl: './app.html', + styleUrl: './app.css', +}) +export class App {} +``` + +#### 4. Create a Home Page + +Start by adding a template at `src/app/home/home.html`: + +```html title="src/app/home/home.html" + + + Home + + + + + + + Home + + + +
+ Ready to create an app? +

+ Start with Ionic + UI Components +

+
+
+``` + +Then, create `src/app/home/home.ts` with the following: + +```ts title="src/app/home/home.ts" +import { Component } from '@angular/core'; +import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + selector: 'app-home', + imports: [IonContent, IonHeader, IonTitle, IonToolbar], + templateUrl: './home.html', + styleUrl: './home.css', +}) +export class HomePage {} +``` + +Finally, add a `src/app/home/home.css` file: + +```css title="src/app/home/home.css" +#container { + text-align: center; + + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +#container strong { + font-size: 20px; + line-height: 26px; +} + +#container p { + font-size: 16px; + line-height: 22px; + + color: #8c8c8c; + + margin: 0; +} + +#container a { + text-decoration: none; +} +``` + +#### 5. Set up Routing + +Update `src/app/app.routes.ts` to add a `home` route: + +```ts title="src/app/app.routes.ts" +import { Routes } from '@angular/router'; +import { HomePage } from './home/home'; + +export const routes: Routes = [ + { + path: '', + redirectTo: 'home', + pathMatch: 'full', + }, + { + path: 'home', + component: HomePage, + }, +]; +``` + +You're all set! Your Ionic Angular app is now configured with full Ionic page support. Run `ng serve` to start your development server and view your app. + +## Next Steps + +Now that you have Ionic Angular integrated into your project, check out: + + + + +

Discover how to handle routing and navigation in Ionic Angular apps using the Angular Router.

+
+ + +

Explore Ionic's rich library of UI components for building beautiful apps.

+
+ + +

Learn how to customize the look and feel of your app with Ionic's powerful theming system.

+
+ + +

Explore how to access native device features and deploy your app to iOS, Android, and the web with Capacitor.

+
+ +
diff --git a/docs/angular/build-options.md b/docs/angular/build-options.md new file mode 100644 index 00000000000..3c6bc082c5b --- /dev/null +++ b/docs/angular/build-options.md @@ -0,0 +1,708 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Build Options + +Developers have two options for using Ionic components: Standalone or Modules. This guide covers both options as well as the benefits and downsides of each approach. + +The Standalone approach uses modern Angular APIs and is the recommended way to build Ionic applications. The Modules approach, including `IonicModule`, is **deprecated** and will be removed in a future major release. New projects should use the Standalone approach. Existing apps will continue to work but should plan to migrate. Refer to [Migrating from Modules to Standalone](#migrating-from-modules-to-standalone) for migration guidance. + +## Standalone + +:::info +Ionic UI components as Angular standalone components is supported starting in Ionic v7.5. +::: + +### Overview + +Developers can use Ionic components as standalone components to take advantage of treeshaking and newer Angular features. This option involves importing specific Ionic components in the Angular components you want to use them in. Developers can use Ionic standalone components even if their Angular application is NgModule-based. + +Refer to the [Standalone Migration Guide](#migrating-from-modules-to-standalone) for instructions on migrating your Ionic app to the Standalone approach. + +**Benefits** + +1. Enables treeshaking so the final build output only includes the code necessary to run your app which reduces overall build size. +2. Avoids the use of NgModules to streamline the development experience and make your code easier to understand. +3. Allows developers to also use newer Angular features such as [ESBuild](https://angular.io/guide/esbuild). + +**Drawbacks** + +1. Ionic components need to be imported into every Angular component they are used in which can be time consuming to set up. + +:::info[Code splitting] +Ionic ships standalone components from a single entry point (`@ionic/angular`). Bundlers such as Webpack and esbuild cannot split code from a single entry point across separate chunks, so the Ionic components you import are included in the main bundle rather than in the chunk for the route or component where they are used. Unused components are still tree-shaken out of the build. +::: + +### Usage with Standalone-based Applications + +:::warning +All Ionic imports should be imported from the `@ionic/angular` submodule. This includes imports such as components, directives, providers, and types. Importing from `@ionic/angular/lazy` may pull in lazy loaded Ionic code which can interfere with treeshaking. +::: + +**Bootstrapping and Configuration** + +Ionic Angular needs to be configured when the Angular application calls `bootstrapApplication` using the `provideIonicAngular` function. Developers can pass any [IonicConfig](../developing/config#ionicconfig) values as an object in this function. Note that `provideIonicAngular` needs to be called even if no custom config is passed. + +```typescript title="main.ts" +import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter } from '@angular/router'; +import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular'; + +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + provideIonicAngular({ mode: 'ios' }), + provideRouter(routes), + ], +}); +``` + +**Components** + +In the example below, we are importing `IonContent` and `IonButton` from `@ionic/angular` and passing them to `imports` for use in the component template. We would get a compiler error if these components were not imported and provided to the `imports` array. + +```typescript title="home.page.ts" +import { Component } from '@angular/core'; +import { IonButton, IonContent } from '@ionic/angular'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], + standalone: true, + imports: [IonButton, IonContent], +}) +export class HomePage { + constructor() {} +} +``` + +**Icons** + +The icon SVG data needs to be defined in the Angular component so it can be loaded correctly. Developers can use the `addIcons` function from `ionicons` to map the SVG data to a string name. Developers can then reference the icon by its string name using the `name` property on `IonIcon`. + +We recommend calling `addIcons` in the Angular component `constructor` so the data is only added if the Angular component is being used. + +For developers using Ionicons 7.2 or newer, passing only the SVG data will cause the string name to be automatically generated. + +```typescript title="home.page.ts" +import { Component } from '@angular/core'; +import { IonIcon } from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], + standalone: true, + imports: [IonIcon], +}) +export class HomePage { + constructor() { + /** + * On Ionicons 7.2+ this icon + * gets mapped to a "logo-ionic" key. + * Alternatively, developers can do: + * addIcons({ 'logo-ionic': logoIonic }); + */ + addIcons({ logoIonic }); + } +} +``` + +Icons can also be registered in entry points such as `app.component.ts` to avoid the need to call `addIcons` multiple times. Developers should be aware that the initial application chunk may increase because the registered icons will need to be loaded at application start. However, if your application uses a small number of icons the impact of this may be minimal. + +```typescript title="app.component.ts" +import { Component } from '@angular/core'; +import { addIcons } from 'ionicons'; +import { logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], + standalone: true, +}) +export class AppComponent { + constructor() { + /** + * Any icons you want to use in your application + * can be registered in app.component.ts and then + * referenced by name anywhere in your application. + */ + addIcons({ logoIonic }); + } +} +``` + +Icons registered in an application entry point can then be referenced by name anywhere in the application. + +```html title="home.page.html" + + +``` + +**Routing** + +Developers who wish to use `routerLink`, `routerAction`, or `routerDirection` on Ionic components should import the `IonRouterLink` directive. Developers who wish to use these routing features on anchor (``) elements should import `IonRouterLinkWithHref` instead. + +```typescript title="home.page.ts" +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { IonButton, IonRouterLink } from '@ionic/angular'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], + standalone: true, + imports: [ + IonButton, + RouterLink, // required to get base routerLink behavior for @angular/router + IonRouterLink, // use IonRouterLinkWithHref if you are using an element instead + ], +}) +export class HomePage {} +``` + +```html title="home.page.html" +Go to Foo Page +``` + +**Testing** + +Ionic Angular's standalone components use ES Modules. As a result, developers using Jest should ensure that ES Modules are transpiled to a format that Jest can use. Developers using Jest should add the following to their Jest config: + + + + +```json + +"transformIgnorePatterns": ["/node_modules/(?!(@ionic/angular|@ionic/core|ionicons|@stencil/core|@angular/*)/)"] +``` + + + + +```json + +"transformIgnorePatterns": ["/node_modules/.pnpm/(?!(@ionic/angular|@ionic/core|ionicons|@stencil/core|@angular/*)@)"] +``` + + + + +### Usage with NgModule-based Applications + +:::warning +All Ionic imports should be imported from the `@ionic/angular` submodule. This includes imports such as components, directives, providers, and types. Importing from `@ionic/angular/lazy` may pull in lazy loaded Ionic code which can interfere with treeshaking. +::: + +**Bootstrapping and Configuration** + +Ionic Angular needs to be configured in the `providers` array of `app.module.ts` using the `provideIonicAngular` function. Developers can pass any [IonicConfig](../developing/config#ionicconfig) values as an object in this function. Note that `provideIonicAngular` needs to be called even if no custom config is passed. + +```typescript title="app.module.ts" +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouteReuseStrategy } from '@angular/router'; + +import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular'; + +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, AppRoutingModule], + providers: [provideIonicAngular(), { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +**Components** + +In the example below, we are importing `IonContent` and `IonButton` from `@ionic/angular` and passing them to `imports` array in the Angular component's NgModule for use in the component template. We would get a compiler error if these components were not imported and provided to the `imports` array. + +```typescript title="home.module.ts" +import { NgModule } from '@angular/core'; +import { IonButton, IonContent } from '@ionic/angular'; +import { HomePage } from './home.page'; + +import { HomePageRoutingModule } from './home-routing.module'; + +@NgModule({ + imports: [IonButton, IonContent, HomePageRoutingModule], + declarations: [HomePage], +}) +export class HomePageModule {} +``` + +**Icons** + +The icon SVG data needs to be defined in the Angular component so it can be loaded correctly. Developers can use the `addIcons` function from `ionicons` to map the SVG data to a string name. Developers can then reference the icon by its string name using the `name` property on `IonIcon`. The `IonIcon` component should be added in `app.module.ts` just like the other Ionic components. + +We recommend calling `addIcons` in the Angular component `constructor` so the data is only added if the Angular component is being used. + +For developers using Ionicons 7.2 or newer, passing only the SVG data will cause the string name to be automatically generated. + +```typescript title="home.page.ts" +import { Component } from '@angular/core'; +import { addIcons } from 'ionicons'; +import { logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], +}) +export class HomePage { + constructor() { + /** + * On Ionicons 7.2+ this icon + * gets mapped to a "logo-ionic" key. + * Alternatively, developers can do: + * addIcons({ 'logo-ionic': logoIonic }); + */ + addIcons({ logoIonic }); + } +} +``` + +Icons can also be registered in entry points such as `app.component.ts` to avoid the need to call `addIcons` multiple times. Developers should be aware that the initial application chunk may increase because the registered icons will need to be loaded at application start. However, if your application uses a small number of icons the impact of this may be minimal. + +```typescript title="app.component.ts" +import { Component } from '@angular/core'; +import { addIcons } from 'ionicons'; +import { logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], +}) +export class AppComponent { + constructor() { + /** + * Any icons you want to use in your application + * can be registered in app.component.ts and then + * referenced by name anywhere in your application. + */ + addIcons({ logoIonic }); + } +} +``` + +Icons registered in an application entry point can then be referenced by name anywhere in the application. + +```html title="home.page.html" + + +``` + +**Routing** + +Developers who wish to use `routerLink`, `routerAction`, or `routerDirection` on Ionic components should import the `IonRouterLink` directive. Developers who wish to use these routing features on anchor (``) elements should import `IonRouterLinkWithHref` instead. + +```typescript title="home.module.ts" +import { NgModule } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { IonButton, IonRouterLink } from '@ionic/angular'; +import { HomePage } from './home.page'; + +import { HomePageRoutingModule } from './home-routing.module'; + +@NgModule({ + imports: [ + IonButton, + RouterLink, // required to get base routerLink behavior for @angular/router + IonRouterLink, // use IonRouterLinkWithHref if you are using an element instead + HomePageRoutingModule, + ], + declarations: [HomePage], +}) +export class HomePageModule {} +``` + +```html title="home.page.html" +Go to Foo Page +``` + +**Testing** + +Ionic Angular's standalone components use ES Modules. As a result, developers using Jest should ensure that ES Modules are transpiled to a format that Jest can use. Developers using Jest should add the following to their Jest config: + + + + +```json + +"transformIgnorePatterns": ["/node_modules/(?!(@ionic/angular|@ionic/core|ionicons|@stencil/core|@angular/*)/)"] +``` + + + + +```json + +"transformIgnorePatterns": ["/node_modules/.pnpm/(?!(@ionic/angular|@ionic/core|ionicons|@stencil/core|@angular/*)@)"] +``` + + + + +## Modules + +:::warning[Deprecation Notice] +The Modules approach, including `IonicModule`, is **deprecated** and will be removed in a future major release. Existing applications will continue to work during the deprecation period but should migrate using the [Standalone migration guide](#migrating-from-modules-to-standalone). New applications should use the [Standalone](#standalone) approach. +::: + +### Overview + +Developers can also use the Modules approach by importing `IonicModule` and calling `IonicModule.forRoot()` in the `imports` array in `app.module.ts`. This registers a version of Ionic where Ionic components will be lazily loaded at runtime. + +**Benefits** + +1. Since components are lazily loaded as needed, developers do not need to spend time manually importing and registering each Ionic component. + +**Drawbacks** + +1. Lazily loading Ionic components means that the compiler does not know which components are needed at build time. This means your final application bundle may be much larger than it needs to be. +2. Developers are unable to use newer Angular features such as [ESBuild](https://angular.io/guide/esbuild). + +### Usage + +In the example below, we are using `IonicModule` to create a lazily loaded version of Ionic. We can then reference any Ionic component without needing to explicitly import it. + +```typescript +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { IonicModule } from '@ionic/angular/lazy'; + +import { AppComponent } from './app.component'; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, IonicModule.forRoot()], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +## Migrating from Modules to Standalone + +:::tip +Try our automated utility for migrating to standalone! + +Refer to the [standalone migration codemods](https://github.com/ionic-team/ionic-angular-standalone-codemods) for instructions on how to get started. All issues related to the migration utility should be filed on the linked repo. +::: + +The Standalone option is newer than the Modules option, so developers may wish to switch during the development of their application. This guide details the steps needed to migrate. + +Migrating to Ionic standalone components must be done all at the same time and cannot be done gradually. The Modules and Standalone approaches use two different build systems of Ionic that cannot be used at the same time. + +Developers are encouraged to try the [automated migration utility](https://github.com/ionic-team/ionic-angular-standalone-codemods), though they can also follow the steps below if they would like to manually migrate their applications. + +### Standalone-based Applications + +Follow these steps if your Angular application is already using the standalone architecture, and you want to use Ionic UI components as standalone components too. + +1. Run `npm install @ionic/angular@latest` to ensure you are running the latest version of Ionic. Ionic UI Standalone Components is supported in Ionic v7.5 or newer. + +2. Run `npm install ionicons@latest` to ensure you are running the latest version of Ionicons. Ionicons v7.2 brings usability improvements that reduce the code boilerplate needed to use icons with standalone components. + +3. Remove the `IonicModule` call in `main.ts` in favor of `provideIonicAngular` imported from `@ionic/angular`. Any config passed to `IonicModule.forRoot` can be passed as an object to this new function. + +```diff title="main.ts" +import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter } from '@angular/router'; +- import { IonicModule, IonicRouteStrategy } from '@ionic/angular/lazy'; ++ import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular'; + +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, { + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + /** + * The custom config serves as an example + * of how to pass a config to provideIonicAngular. + * You do not need to set "mode: 'ios'" to + * use Ionic standalone components. + */ +- importProvidersFrom(IonicModule.forRoot({ mode: 'ios' })), ++ provideIonicAngular({ mode: 'ios' }), + provideRouter(routes), + ], +}); +``` + +4. Remove any references to `IonicModule` found elsewhere in your application. + +5. Update any existing imports from `@ionic/angular/lazy` to import from `@ionic/angular` instead. + +```diff +- import { Platform } from '@ionic/angular/lazy'; ++ import { Platform } from '@ionic/angular'; +``` + +6. Add imports for each Ionic component in the Angular component where they are used. Be sure to pass the imports to the `imports` array on your Angular component. + +```diff title="app.component.ts" +import { Component } from '@angular/core'; ++ import { IonApp, IonRouterOutlet } from '@ionic/angular'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], + standalone: true, ++ imports: [IonApp, IonRouterOutlet], +}) +export class AppComponent { + constructor() {} +} +``` + +7. If you are using Ionicons, define the icon SVG data used in each Angular component using `addIcons`. This allows you to continue referencing icons by string name in your component template. Note that you will need to do this for any additional icons added. + +```diff title="test.component.ts" +import { Component } from '@angular/core'; ++ import { IonIcon } from '@ionic/angular'; ++ import { addIcons } from 'ionicons'; ++ import { alarm, logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], + standalone: true, ++ imports: [IonIcon], +}) +export class TestComponent { + constructor() { ++ addIcons({ alarm, logoIonic }); + } +} +``` + +8. Remove the following code from your `angular.json` file if present. Note that it may appear multiple times. + +```diff title="angular.json" +- { +- "glob": "**/*.svg", +- "input": "node_modules/ionicons/dist/ionicons/svg", +- "output": "./svg" +- } +``` + +9. If you are using `routerLink`, `routerDirection`, or `routerAction` be sure to import the `IonRouterLink` directive for Ionic components or `IonRouterLinkWithHref` directive for `` elements. + +```diff title="test.component.ts" +import { Component } from '@angular/core'; +- import { IonButton } from '@ionic/angular'; ++ import { IonButton, IonRouterLink } from '@ionic/angular'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], + standalone: true, + imports: [ + IonButton, ++ IonRouterLink + ], +}) +export class TestComponent {} +``` + +10. If you are using VSCode it is recommended to ignore the `@ionic/angular/common` and `@ionic/angular/lazy` module specifiers for import recommendations. + +```json title=".vscode/settings.json" +{ + "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/lazy"] +} +``` + +### NgModule-based Applications + +Follow these steps if your Angular application is still using the NgModule architecture, but you want to adopt Ionic UI components as standalone components now. + +1. Run `npm install @ionic/angular@latest` to ensure you are running the latest version of Ionic. Ionic UI Standalone Components is supported in Ionic v7.5 or newer. + +2. Run `npm install ionicons@latest` to ensure you are running the latest version of Ionicons. Ionicons v7.2 brings usability improvements that reduce the code boilerplate needed to use icons with standalone components. + +3. Remove the `IonicModule` call in `app.module.ts` in favor of `provideIonicAngular` imported from `@ionic/angular`. Any config passed to `IonicModule.forRoot` can be passed as an object to this new function. + +```diff title="app.module.ts" +import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter } from '@angular/router'; +- import { IonicModule, IonicRouteStrategy } from '@ionic/angular/lazy'; ++ import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular'; + +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +@NgModule({ + declarations: [AppComponent], +- imports: [BrowserModule, IonicModule.forRoot({ mode: 'ios' }), AppRoutingModule], ++ imports: [BrowserModule, AppRoutingModule], + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + /** + * The custom config serves as an example + * of how to pass a config to provideIonicAngular. + * You do not need to set "mode: 'ios'" to + * use Ionic standalone components. + */ ++ provideIonicAngular({ mode: 'ios' }), + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +4. Remove any references to `IonicModule` found elsewhere in your application. + +5. Update any existing imports from `@ionic/angular/lazy` to import from `@ionic/angular` instead. + +```diff +- import { Platform } from '@ionic/angular/lazy'; ++ import { Platform } from '@ionic/angular'; +``` + +6. Add imports for each Ionic component in the NgModule for the Angular component where they are used. Be sure to pass the components to the `imports` array on the module. + +```diff title="app.module.ts" +import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter } from '@angular/router'; +- import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular'; ++ import { provideIonicAngular, IonicRouteStrategy, IonApp, IonRouterOutlet } from '@ionic/angular'; + +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +@NgModule({ + declarations: [AppComponent], +- imports: [BrowserModule, AppRoutingModule], ++ imports: [BrowserModule, AppRoutingModule, IonApp, IonRouterOutlet], + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + provideIonicAngular({ mode: 'ios' }) + ], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +For example, all modules that are using Ionic components need to have the Ionic components imported in their component module. + +```diff title="home.module.ts" +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HomePage } from './home.page'; + +import { HomePageRoutingModule } from './home-routing.module'; + ++ import { IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + HomePageRoutingModule, ++ IonContent, ++ IonHeader, ++ IonTitle, ++ IonToolbar + ], + declarations: [HomePage] +}) +export class HomePageModule {} +``` + +7. If you are using Ionicons, define the icon SVG data used in each Angular component using `addIcons`. This allows you to continue referencing icons by string name in your component template. Note that you will need to do this for any additional icons added. The `IonIcon` component should still be provided in the NgModule. + +```diff title="test.component.ts" +import { Component } from '@angular/core'; ++ import { addIcons } from 'ionicons'; ++ import { alarm, logoIonic } from 'ionicons/icons'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.scss'], +}) +export class TestComponent { + constructor() { ++ addIcons({ alarm, logoIonic }); + } +} +``` + +```diff title="test.module.ts" +import { NgModule } from '@angular/core'; +import { TestComponent } from './test.component'; ++ import { IonIcon } from '@ionic/angular'; + +@NgModule({ + imports: [ ++ IonIcon, + ], + declarations: [TestComponent] +}) +export class TestComponentModule {} +``` + +8. Remove the following code from your `angular.json` file if present. Note that it may appear multiple times. + +```diff title="angular.json" +- { +- "glob": "**/*.svg", +- "input": "node_modules/ionicons/dist/ionicons/svg", +- "output": "./svg" +- } +``` + +9. If you are using `routerLink`, `routerDirection`, or `routerAction` be sure to import the `IonRouterLink` directive for Ionic components or `IonRouterLinkWithHref` directive for `` elements. + +```diff title="test.module.ts" +import { NgModule } from '@angular/core'; +import { TestComponent } from './test.component'; +- import { IonButton } from '@ionic/angular'; ++ import { IonButton, IonRouterLink } from '@ionic/angular'; + +@NgModule({ + imports: [ + IonButton, ++ IonRouterLink, + ], + declarations: [TestComponent] +}) +``` + +10. If you are using VSCode it is recommended to ignore the `@ionic/angular/common` and `@ionic/angular/lazy` module specifiers for import recommendations. + +```json title=".vscode/settings.json" +{ + "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/lazy"] +} +``` diff --git a/docs/angular/injection-tokens.md b/docs/angular/injection-tokens.md new file mode 100644 index 00000000000..2367036ab74 --- /dev/null +++ b/docs/angular/injection-tokens.md @@ -0,0 +1,177 @@ +--- +title: Angular Injection Tokens +sidebar_label: Injection Tokens +--- + + + Angular Injection Tokens | Access Ionic Elements via Dependency Injection + + + +Ionic provides Angular injection tokens that allow you to access Ionic elements through Angular's dependency injection system. This provides a more Angular-idiomatic way to interact with Ionic components programmatically. + +## Benefits + +Using injection tokens provides several advantages: + +- **Type Safety**: Full TypeScript support with proper typing for the modal element +- **Angular Integration**: Works seamlessly with Angular's dependency injection system +- **Simplified Code**: Eliminates the need for `ViewChild` queries or manual element references +- **Better Testing**: Easier to mock and test components that use injection tokens + +## IonModalToken + +The `IonModalToken` injection token allows you to inject a reference to the current modal element directly into your Angular components. This is particularly useful when you need to programmatically control modal behavior, listen to modal events, or access modal properties. + +Starting in `@ionic/angular` v8.7.0, you can use this injection token to streamline modal interactions in your Angular applications. + +### Basic Usage + +To use the `IonModalToken`, inject it into your component's constructor: + +```tsx +import { Component, inject } from '@angular/core'; +import { IonButton, IonContent, IonHeader, IonModalToken, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + selector: 'app-modal', + template: ` + + + Modal Content + + + +

This is modal content

+ Close Modal +
+ `, + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonButton], +}) +export class ModalComponent { + private modalToken = inject(IonModalToken); + + closeModal() { + this.modalToken.dismiss(); + } +} +``` + +### Listening to Modal Events + +You can use the injected modal reference to listen to modal lifecycle events: + +```tsx +import { Component, inject, OnInit } from '@angular/core'; +import { IonButton, IonContent, IonHeader, IonModalToken, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + selector: 'app-modal', + template: ` + + + Modal with Events + + + +

Check the console for modal events

+ Close +
+ `, + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonButton], +}) +export class ModalComponent implements OnInit { + private modalToken = inject(IonModalToken); + + ngOnInit() { + this.modalToken.addEventListener('ionModalWillDismiss', (event) => { + console.log('Modal will dismiss:', event.detail); + }); + + this.modalToken.addEventListener('ionModalDidDismiss', (event) => { + console.log('Modal did dismiss:', event.detail); + }); + } + + closeModal() { + this.modalToken.dismiss({ result: 'closed by button' }); + } +} +``` + +### Accessing Modal Properties + +The injected modal reference provides access to all modal properties and methods: + +```tsx +import { Component, inject, OnInit } from '@angular/core'; +import { IonButton, IonContent, IonHeader, IonModalToken, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + selector: 'app-modal', + template: ` + + + Modal Properties + + + +

Modal ID: {{ modalId }}

+ Toggle Backdrop Dismiss: {{ backdropDismiss }} +
+ `, + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonButton], +}) +export class ModalComponent implements OnInit { + private modalToken = inject(IonModalToken); + + modalId = ''; + backdropDismiss = true; + + ngOnInit() { + this.modalId = this.modalToken.id || 'No ID'; + this.backdropDismiss = this.modalToken.backdropDismiss; + } + + toggleBackdropDismiss() { + this.backdropDismiss = !this.backdropDismiss; + this.modalToken.backdropDismiss = this.backdropDismiss; + } +} +``` + +### Opening a Modal with Injection Token Content + +When opening a modal that uses the injection token, you can pass the component directly to the modal controller: + +```tsx +import { Component, inject } from '@angular/core'; +import { IonContent, IonButton, ModalController } from '@ionic/angular'; +import { ModalComponent } from './modal.component'; + +@Component({ + selector: 'app-home', + template: ` + + Open Modal + + `, +}) +export class HomePage { + private modalController = inject(ModalController); + + async openModal() { + const myModal = await this.modalController.create({ + component: ModalComponent, + componentProps: { + // Any props you want to pass to the modal content + }, + }); + + await myModal.present(); + } +} +``` diff --git a/docs/angular/lifecycle.md b/docs/angular/lifecycle.md new file mode 100644 index 00000000000..e56f65dbbf0 --- /dev/null +++ b/docs/angular/lifecycle.md @@ -0,0 +1,103 @@ +--- +title: Ionic Page Life Cycle +sidebar_label: Lifecycle +--- + + + Angular Page Component Life Cycle - App Events Documentation + + + +This guide covers how the page life cycle works in an app built with Ionic and Angular. + +![Flowchart illustrating the Ionic page life cycle events and their sequence.](/img/guides/lifecycle/ioniclifecycle.png 'Ionic Lifecycle Diagram') + +## Angular Life Cycle Events + +Ionic embraces the life cycle events provided by Angular. The two Angular events you will find using the most are: + +| Event Name | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ngOnInit` | Fired once during component initialization. This event can be used to initialize local members and make calls into services that only need to be done once. | +| `ngOnDestroy` | Fired right before Angular destroys the view. Useful for cleanup like unsubscribing from observables. | + +For more info on the Angular Component Life Cycle events, visit their [component lifecycle docs](https://angular.io/guide/lifecycle-hooks). + +:::note +Components that use `ion-nav` or `ion-router-outlet` should not use the `OnPush` change detection strategy. Doing so will prevent lifecycle hooks such as `ngOnInit` from firing. Additionally, asynchronous state changes may not render properly. +::: + +## Ionic Page Events + +In addition to the Angular life cycle events, Ionic Angular provides a few additional events that you can use: + +| Event Name | Description | +| ------------------ | ------------------------------------------------------------------ | +| `ionViewWillEnter` | Fired when the component routing to is about to animate into view. | +| `ionViewDidEnter` | Fired when the component routing to has _finished_ animating. | +| `ionViewWillLeave` | Fired when the component routing _from_ is about to animate. | +| `ionViewDidLeave` | Fired when the component routing _from_ has _finished_ animating. | + +These lifecycles are only called on components directly mapped by a router. This means if `/pageOne` maps to `PageOneComponent`, then Ionic lifecycles will be called on `PageOneComponent` but will not be called on any child components that `PageOneComponent` may render. + +The difference between `ionViewWillEnter` and `ionViewDidEnter` is when they fire. The former fires right after `ngOnInit` but before the page transition begins, and the latter directly after the transition ends. + +For `ionViewWillLeave` and `ionViewDidLeave`, `ionViewWillLeave` gets called directly before the transition away from the current page begins, and `ionViewDidLeave` does not get called until after the new page gets successfully transitioned into (after the new pages `ionViewDidEnter` fires). + +![Animated GIF showing Ionic page life cycle events in a console log as a page transition occurs.](/img/guides/lifecycle/ioniclifecycle.gif 'Ionic Lifecycle Animation') + +## How Ionic Handles the Life of a Page + +Ionic has its router outlet, called ``. This outlet extends Angular's `` with some additional functionality to enable better experiences for mobile devices. + +When an app is wrapped in ``, Ionic treats navigation a bit differently. When you navigate to a new page, Ionic will keep the old page in the existing DOM, but hide it from your view and transition the new page. The reason we do this is two-fold: + +1. We can maintain the state of the old page (data on the screen, scroll position, etc..) +2. We can provide a smoother transition back to the page since it is already there and doesn't need to be recreated. + +Pages are only removed from the DOM when they are "popped", for instance, by pressing the back button in the UI or the browsers back button. + +Because of this special handling, the `ngOnInit` and `ngOnDestroy` methods might not fire when you would usually think they should. + +`ngOnInit` will only fire each time the page is freshly created, but not when navigated back to the page. For instance, navigating between each page in a tabs interface will only call each page's `ngOnInit` method once, but not on subsequent visits. `ngOnDestroy` will only fire when a page "popped". + +## Route Guards + +In Ionic 3, there were a couple of additional life cycle methods that were useful to control when a page could be entered (`ionViewCanEnter`) and left (`ionViewCanLeave`). These could be used to protect pages from unauthorized users and to keep a user on a page when you don't want them to leave (like during a form fill). + +These methods were removed in Ionic 4 in favor of using Angular's Route Guards. + +A route guard helps determine if a particular action can be taken against a route. They are classes that implement a certain interface. The `CanActivate` and `CanDeactivate` interfaces can be used to implement the same type of logic that the removed events `ionViewCanEnter` and `ionViewCanLeave` did. + +```tsx +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + return this.authService.isAuthenticated(); + } +} +``` + +To use this guard, add it to the appropriate param in the route definition: + +```tsx +{ path: 'settings', canActivate: [AuthGuard], loadChildren: '...', } +``` + +For more info on how to use route guards, go to Angular's [router documentation](https://angular.io/guide/router). + +## Guidance for Each Life Cycle Method + +Below are some tips on use cases for each of the life cycle events. + +- `ngOnInit` - Initialize your component and load data from services that don't need refreshing on each subsequent visit. +- `ionViewWillEnter` - Since `ionViewWillEnter` is called every time the view is navigated to (regardless if initialized or not), it's a good method to load data from services. However, if your data comes back during the animation, it can start lots of DOM manipulation, which can cause some janky animations. +- `ionViewDidEnter` - If you encounter performance problems from using `ionViewWillEnter` when loading data, you can do your data calls in `ionViewDidEnter` instead. This event won't fire until after the page is visible by the user, however, so you might want to use either a loading indicator or a skeleton screen, so content doesn't flash in un-naturally after the transition is complete. +- `ionViewWillLeave` - Can be used for cleanup, like unsubscribing from observables. Since `ngOnDestroy` might not fire when you navigate from the current page, put your cleanup code here if you don't want it active while the screen is not in view. +- `ionViewDidLeave` - When this event fires, you know the new page has fully transitioned in, so any logic you might not normally do when the view is visible can go here. +- `ngOnDestroy` - Cleanup logic for your pages that you don't want to clean up in `ionViewWillLeave`. diff --git a/docs/angular/navigation.md b/docs/angular/navigation.md new file mode 100644 index 00000000000..3d96837883d --- /dev/null +++ b/docs/angular/navigation.md @@ -0,0 +1,477 @@ +--- +title: Angular Navigation +sidebar_label: Navigation/Routing +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + + + Angular Navigation: How Routing & Redirects Work in Angular Apps + + + +This guide covers how routing works in an app built with Ionic and Angular. + +The Angular Router is one of the most important libraries in an Angular application. Without it, apps would be single view/single context apps or would not be able to maintain their navigation state on browser reloads. With Angular Router, we can create rich apps that are linkable and have rich animations (when paired with Ionic of course). Let's walk through the basics of the Angular Router and how we can configure it for Ionic apps. + +## A simple Route + +For most apps, having some sort of route is often required. The most basic configuration looks a bit like this: + +```tsx + +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [ + ... + RouterModule.forRoot([ + { path: '', component: LoginComponent }, + { path: 'detail', component: DetailComponent }, + ]) + ], +}) +``` + +The simplest breakdown for what we have here is a path/component lookup. When our app loads, the router kicks things off by reading the URL the user is trying to load. In our sample, our route looks for `''`, which is essentially our index route. So for this, we load the `LoginComponent`. Fairly straight forward. This pattern of matching paths with a component continues for every entry we have in the router config. But what if we wanted to load a different path on our initial load? + +## Handling Redirects + +For this we can use router redirects. Redirects work the same way that a typical route object does, but just includes a few different keys. + +```tsx +[ + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: 'login', component: LoginComponent }, + { path: 'detail', component: DetailComponent }, +]; +``` + +In our redirect, we look for the index path of our app. Then if we load that, we redirect to the `login` route. The last key of `pathMatch` is required to tell the router how it should look up the path. + +Since we use `full`, we're telling the router that we should compare the full path, even if ends up being something like `/route1/route2/route3`. Meaning that if we have: + +```tsx +{ path: '/route1/route2/route3', redirectTo: 'login', pathMatch: 'full' }, +{ path: 'login', component: LoginComponent }, +``` + +And load `/route1/route2/route3` we'll redirect. But if we loaded `/route1/route2/route4`, we won't redirect, as the paths don't match fully. + +Alternatively, if we used: + +```tsx +{ path: '/route1/route2', redirectTo: 'login', pathMatch: 'prefix' }, +{ path: 'login', component: LoginComponent }, +``` + +Then load both `/route1/route2/route3` and `/route1/route2/route4`, we'll be redirected for both routes. This is because `pathMatch: 'prefix'` will match only part of the path. + +## Navigating to different routes + +Talking about routes is good and all, but how does one actually navigate to said routes? For this, we can use the `routerLink` directive. Let's go back and take our simple router setup from earlier: + +```ts +RouterModule.forRoot([ + { path: '', component: LoginComponent }, + { path: 'detail', component: DetailComponent }, +]); +``` + +Now from the `LoginComponent`, we can use the following HTML to navigate to the detail route. + +```html + + + Login + + + + + Go to detail + +``` + +The important part here is the `ion-button` and `routerLink` directive. RouterLink works on a similar idea as typical `href`s, but instead of building out the URL as a string, it can be built as an array, which can provide more complicated paths. + +We also can programmatically navigate in our app by using the router API. + +```tsx +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + ... +}) +export class LoginComponent { + + constructor(private router: Router){} + + navigate(){ + this.router.navigate(['/detail']) + } +} +``` + +Both options provide the same navigation mechanism, just fitting different use cases. + +### Navigating using LocationStrategy.historyGo + +Angular Router has a [LocationStrategy.historyGo](https://angular.io/api/common/LocationStrategy#historyGo) method that allows developers to move forward or backward through the application history. Let's walk through an example. + +Say you have the following application history: + +`/pageA` --> `/pageB` --> `/pageC` + +If you were to call `LocationStrategy.historyGo(-2)` on `/pageC`, you would be brought back to `/pageA`. If you then called `LocationStrategy.historyGo(2)`, you would be brought to `/pageC`. + +An key characteristic of `LocationStrategy.historyGo()` is that it expects your application history to be linear. This means that `LocationStrategy.historyGo()` should not be used in applications that make use of non-linear routing. Refer to [Linear Routing versus Non-Linear Routing](#linear-routing-versus-non-linear-routing) for more information. + +## Lazy loading routes + +Now the current way our routes are setup makes it so they are included in the same chunk as the root app.module, which is not ideal. Instead, the router has a setup that allows the components to be isolated to their own chunks. + +```tsx + +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [ + ... + RouterModule.forRoot([ + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: 'login', loadChildren: () => import('./login/login.module').then(m => m.LoginModule) }, + { path: 'detail', loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule) } + ]) + ], +}) +``` + +While similar, the `loadChildren` property is a way to reference a module by using native import instead of a component directly. In order to do this though, we need to create a module for each of the components. + +```tsx +... +import { RouterModule } from '@angular/router'; +import { LoginComponent } from './login.component'; + +@NgModule({ + imports: [ + ... + RouterModule.forChild([ + { path: '', component: LoginComponent }, + ]) + ], +}) +``` + +:::note +We're excluding some additional content and only including the necessary parts. +::: + +Here, we have a typical Angular Module setup, along with a RouterModule import, but we're now using `forChild` and declaring the component in that setup. With this setup, when we run our build, we will produce separate chunks for both the app component, the login component, and the detail component. + +## Standalone Components + +Standalone components allow developers to lazy load a component on a route without having to declare the component to an Angular module. + +Developers can use the existing syntax for standalone component routing from Angular: + +```ts +@NgModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'standalone-route', + loadComponent: () => import('./path/to/my-component.component').then((c) => c.MyComponent), + }, + ]), + ], +}) +export class AppRoutingModule {} +``` + +:::tip +If you are using `routerLink`, `routerDirection`, or `routerAction` be sure to also import the `IonRouterLink` directive for Ionic components or the `IonRouterLinkWithHref` directive for `
` elements. An example of this is available in the [Ionic Angular Build Options docs](./build-options.md#migrating-from-modules-to-standalone). +::: + +To get started with standalone components [visit Angular's official docs](https://angular.io/guide/standalone-components). + +## Live Example + +import NavigationPlayground from '@site/static/usage/v9/navigation/index.md'; + + + +## Linear Routing versus Non-Linear Routing + +### Linear Routing + +If you have built a web app that uses routing, you likely have used linear routing before. Linear routing means that you can move forward or backward through the application history by pushing and popping pages. + +The following is an example of linear routing in a mobile app: + + + +The application history in this example has the following path: + +`Accessibility` --> `VoiceOver` --> `Speech` + +When we press the back button, we follow that same routing path except in reverse. Linear routing is helpful in that it allows for simple and predictable routing behaviors. It also means we can use router Angular Router APIs such as [LocationStrategy.historyGo()](#navigating-using-locationstrategy). + +The downside of linear routing is that it does not allow for complex user experiences such as tab views. This is where non-linear routing comes into play. + +### Non-Linear Routing + +Non-linear routing is a concept that may be new to many web developers learning to build mobile apps with Ionic. + +Non-linear routing means that the view that the user should go back to is not necessarily the previous view that was displayed on the screen. + +The following is an example of non-linear routing: + + + +In the example above, we start on the `Originals` tab. Tapping a card brings us to the `Ted Lasso` view within the `Originals` tab. + +From here, we switch to the `Search` tab. Then, we tap the `Originals` tab again and are brought back to the `Ted Lasso` view. At this point, we have started using non-linear routing. + +Why is this non-linear routing? The previous view we were on was the `Search` view. However, pressing the back button on the `Ted Lasso` view should bring us back to the root `Originals` view. This happens because each tab in a mobile app is treated as its own stack. The [Working with Tabs](#working-with-tabs) sections goes over this in more detail. + +If tapping the back button simply called `LocationStrategy.historyGo(-1)` from the `Ted Lasso` view, we would be brought back to the `Search` view which is not correct. + +Non-linear routing allows for sophisticated user flows that linear routing cannot handle. However, certain linear routing APIs such as `LocationStrategy.historyGo()` cannot be used in this non-linear environment. This means that `LocationStrategy.historyGo()` should not be used when using tabs or nested outlets. + +### Which one should I choose? + +We recommend keeping your application as simple as possible until you need to add non-linear routing. Non-linear routing is very powerful, but it also adds a considerable amount of complexity to mobile applications. + +The two most common uses of non-linear routing is with tabs and nested `ion-router-outlet`s. We recommend only using non-linear routing if your application meets the tabs or nested router outlet use cases. + +For more on tabs, please refer to [Working with Tabs](#working-with-tabs). + +For more on nested router outlets, please refer to [Nested Routes](#nested-routes). + +## Shared URLs versus Nested Routes + +A common point of confusion when setting up routing is deciding between shared URLs or nested routes. This part of the guide will explain both and help you decide which one to use. + +### Shared URLs + +Shared URLs is a route configuration where routes have pieces of the URL in common. The following is an example of a shared URL configuration: + +```tsx +const routes: Routes = [ + { + path: 'dashboard', + component: DashboardMainPage, + }, + { + path: 'dashboard/stats', + component: DashboardStatsPage, + }, +]; +``` + +The above routes are considered "shared" because they reuse the `dashboard` piece of the URL. + +### Nested Routes + +Nested Routes is a route configuration where routes are listed as children of other routes. The following is an example of a nested route configuration: + +```tsx +const routes: Routes = [ + { + path: 'dashboard', + component: DashboardRouterOutlet, + children: [ + { + path: '', + component: DashboardMainPage, + }, + { + path: 'stats', + component: DashboardStatsPage, + }, + ], + }, +]; +``` + +The above routes are nested because they are in the `children` array of the parent route. Notice that the parent route renders the `DashboardRouterOutlet` component. When you nest routes, you need to render another instance of `ion-router-outlet`. + +### Which one should I choose? + +Shared URLs are great when you want to transition from page A to page B while preserving the relationship between the two pages in the URL. In our previous example, a button on the `/dashboard` page could transition to the `/dashboard/stats` page. The relationship between the two pages is preserved because of a) the page transition and b) the url. + +Nested routes should be used when you want to render content in outlet A while also rendering sub-content inside of a nested outlet B. The most common use case you will run into is tabs. When you load up a tabs Ionic starter application, the first `ion-router-outlet` renders the `ion-tab-bar` and `ion-tabs` components. The `ion-tabs` component renders another `ion-router-outlet` which is responsible for rendering the contents of each tab. + +There are very few use cases in which nested routes make sense in mobile applications. When in doubt, use the shared URL route configuration. We strongly caution against using nested routing in contexts other than tabs as it can quickly make navigating your app confusing. + +## Working with Tabs + +With Tabs, the Angular Router provides Ionic the mechanism to know what components should be loaded, but the heavy lifting is actually done by the tabs component. Let's walk through a simple example. + +```ts +const routes: Routes = [ + { + path: 'tabs', + component: TabsPage, + children: [ + { + path: 'tab1', + children: [ + { + path: '', + loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule), + }, + ], + }, + { + path: '', + redirectTo: '/tabs/tab1', + pathMatch: 'full', + }, + ], + }, + { + path: '', + redirectTo: '/tabs/tab1', + pathMatch: 'full', + }, +]; +``` + +Here we have a "tabs" path that we load. In this example we call the path "tabs", but the name of the paths can be changed. They can be called whatever fits your app. In that route object, we can define a child route as well. In this example, the top level child route "tab1" acts as our "outlet", and can load additional child routes. For this example, we have a single sub-child-route, which just loads a new component. The markup for the tab is as follows: + +```html + + + + + Tab One + + + +``` + +If you've built apps with Ionic before, this should feel familiar. We create a `ion-tabs` component, and provide a `ion-tab-bar`. The `ion-tab-bar` provides a `ion-tab-button` with a `tab` property that is associated with the tab "outlet" in the router config. Note that the latest version of `@ionic/angular` no longer requires ``, but instead allows developers to fully customize the tab bar, and the single source of truth lives within the router configuration. + +### How Tabs in Ionic Work + +Each tab in Ionic is treated as an individual navigation stack. This means if you have three tabs in your application, each tab has its own navigation stack. Within each stack you can navigate forwards (push a view) and backwards (pop a view). + +This behavior is important to note as it is different than most tab implementations that are found in other web based UI libraries. Other libraries typically manage tabs as one single history stack. + +Since Ionic is focused on helping developers build mobile apps, the tabs in Ionic are designed to match native mobile tabs as closely as possible. As a result, there may be certain behaviors in Ionic's tabs that differ from tabs implementations in other UI libraries. Read on to learn more about some of these differences. + +### Child Routes within Tabs + +When adding additional routes to tabs you should write them as sibling routes with the parent tab as the path prefix. The example below defines the `/tabs/tab1/view` route as a sibling of the `/tabs/tab1` route. Since this new route has the `tab1` prefix, it will be rendered inside of the `Tabs` component, and Tab 1 will still be selected in the `ion-tab-bar`. + +```ts +const routes: Routes = [ + { + path: 'tabs', + component: TabsPage, + children: [ + { + path: 'tab1', + children: [ + { + path: '', + loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule), + }, + ], + }, + { + path: 'tab1/view', + children: [ + { + path: '', + loadChildren: () => import('../tab1/tab1view.module').then((m) => m.Tab1ViewPageModule), + }, + ], + }, + { + path: 'tab2', + children: [ + { + path: '', + loadChildren: () => import('../tab2/tab2.module').then((m) => m.Tab2PageModule), + }, + ], + }, + { + path: 'tab3', + children: [ + { + path: '', + loadChildren: () => import('../tab3/tab3.module').then((m) => m.Tab3PageModule), + }, + ], + }, + ], + }, + { + path: '', + redirectTo: '/tabs/tab1', + pathMatch: 'full', + }, +]; +``` + +### Switching Between Tabs + +Since each tab is its own navigation stack, it is important to note that these navigation stacks should never interact. This means that there should never be a button in Tab 1 that routes a user to Tab 2. In other words, tabs should only be changed by the user tapping a tab button in the tab bar. + +A good example of this in practice is the iOS App Store and Google Play Store mobile applications. These apps both provide tabbed interfaces, but neither one ever routes the user across tabs. For example, the "Games" tab in the iOS App Store app never directs users to the "Search" tab and vice versa. + +Let's go over a couple common mistakes that are made with tabs. + +**A Settings Tab That Multiple Tabs Reference** + +A common practice is to create a Settings view as its own tab. This is great if developers need to present several nested settings menus. However, other tabs should never try to route to the Settings tab. As we mentioned above, the only way that the Settings tab should be activated is by a user tapping the appropriate tab button. + +If you find that your tabs need to reference the Settings tab, we recommend making the Settings view a modal by using `ion-modal`. This is a practice found in the iOS App Store app. With this approach, any tab can present the modal without breaking the mobile tabs pattern of each tab being its own stack. + +The example below shows how the iOS App Store app handles presenting an "Account" view from multiple tabs. By presenting the "Account" view in a modal, the app can work within the mobile tabs best practices to show the same view across multiple tabs. + + + +**Reusing Views Across Tabs** + +Another common practice is to present the same view in multiple tabs. Developers often try to do this by having the view contained in a single tab, with other tabs routing to that tab. As we mentioned above, this breaks the mobile tabs pattern and should be avoided. + +Instead, we recommend having routes in each tab that reference the same component. This is a practice done in popular apps like Spotify. For example, you can access an album or podcast from the "Home", "Search", and "Your Library" tabs. When accessing the album or podcast, users stay within that tab. The app does this by creating routes per tab and sharing a common component in the codebase. + +The example below shows how the Spotify app reuses the same album component to show content in multiple tabs. Notice that each screenshot shows the same album but from a different tab. + +| Home Tab | Search Tab | +| :-------------------------------------------------: | :---------------------------------------------------: | +| | | diff --git a/docs/angular/overlays.md b/docs/angular/overlays.md new file mode 100644 index 00000000000..911063b5362 --- /dev/null +++ b/docs/angular/overlays.md @@ -0,0 +1,213 @@ +--- +title: Overlay Components +sidebar_label: Overlays +--- + + + Angular Overlay Components: Modals, Popovers with Custom Injectors + + + +Ionic provides overlay components such as modals and popovers that display content on top of your application. In Angular, these overlays can be created using controllers like `ModalController` and `PopoverController`. + +## Creating Overlays + +Overlays can be created programmatically using their respective controllers: + +```typescript +import { Component } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { MyModalComponent } from './my-modal.component'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', +}) +export class HomeComponent { + constructor(private modalController: ModalController) {} + + async openModal() { + const modal = await this.modalController.create({ + component: MyModalComponent, + componentProps: { + title: 'My Modal', + }, + }); + await modal.present(); + } +} +``` + +## Custom Injectors + +By default, overlay components use the root injector for dependency injection. This means that services or tokens provided at the route level or within a specific component tree are not accessible inside the overlay. + +The `injector` option allows you to pass a custom Angular `Injector` when creating a modal or popover. This enables overlay components to access services and tokens that are not available in the root injector. + +### Use Cases + +Custom injectors are useful when you need to: + +- Access route-scoped services from within an overlay +- Use Angular CDK's `Dir` directive for bidirectional text support +- Access any providers that are not registered at the root level + +### Usage + +To use a custom injector, pass it to the `create()` method: + +```typescript +import { Component, Injector } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { MyModalComponent } from './my-modal.component'; +import { MyRouteService } from './my-route.service'; + +@Component({ + selector: 'app-feature', + templateUrl: './feature.component.html', + providers: [MyRouteService], // Service provided at route level +}) +export class FeatureComponent { + constructor(private modalController: ModalController, private injector: Injector) {} + + async openModal() { + const modal = await this.modalController.create({ + component: MyModalComponent, + injector: this.injector, // Pass the component's injector + }); + await modal.present(); + } +} +``` + +The modal component can now inject `MyRouteService`: + +```typescript +import { Component, inject } from '@angular/core'; +import { MyRouteService } from '../my-route.service'; + +@Component({ + selector: 'app-my-modal', + templateUrl: './my-modal.component.html', +}) +export class MyModalComponent { + private myRouteService = inject(MyRouteService); +} +``` + +### Creating a Custom Injector + +You can also create a custom injector with specific providers: + +```typescript +import { Component, Injector } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { MyModalComponent } from './my-modal.component'; +import { MyService } from './my.service'; + +@Component({ + selector: 'app-feature', + templateUrl: './feature.component.html', +}) +export class FeatureComponent { + constructor(private modalController: ModalController, private injector: Injector) {} + + async openModal() { + const myService = new MyService(); + myService.configure({ someOption: true }); + + const customInjector = Injector.create({ + providers: [{ provide: MyService, useValue: myService }], + parent: this.injector, + }); + + const modal = await this.modalController.create({ + component: MyModalComponent, + injector: customInjector, + }); + await modal.present(); + } +} +``` + +### Using with Angular CDK Directionality + +A common use case is providing the Angular CDK `Dir` directive to overlays for bidirectional text support: + +```typescript +import { Component, Injector } from '@angular/core'; +import { Dir } from '@angular/cdk/bidi'; +import { ModalController } from '@ionic/angular'; +import { MyModalComponent } from './my-modal.component'; + +@Component({ + selector: 'app-feature', + templateUrl: './feature.component.html', +}) +export class FeatureComponent { + constructor(private modalController: ModalController, private injector: Injector) {} + + async openModal() { + const modal = await this.modalController.create({ + component: MyModalComponent, + injector: this.injector, // Includes Dir from component tree + }); + await modal.present(); + } +} +``` + +### Popover Controller + +The `PopoverController` supports the same `injector` option: + +```typescript +import { Component, Injector } from '@angular/core'; +import { PopoverController } from '@ionic/angular'; +import { MyPopoverComponent } from './my-popover.component'; + +@Component({ + selector: 'app-feature', + templateUrl: './feature.component.html', +}) +export class FeatureComponent { + constructor(private popoverController: PopoverController, private injector: Injector) {} + + async openPopover(event: Event) { + const popover = await this.popoverController.create({ + component: MyPopoverComponent, + event: event, + injector: this.injector, + }); + await popover.present(); + } +} +``` + +## Angular Options Types + +Ionic Angular exports its own `ModalOptions` and `PopoverOptions` types that extend the core options with Angular-specific properties like `injector`: + +- `ModalOptions` - Extends core `ModalOptions` with the `injector` property +- `PopoverOptions` - Extends core `PopoverOptions` with the `injector` property + +These types are exported from `@ionic/angular` and `@ionic/angular/lazy`: + +```typescript +import type { ModalOptions, PopoverOptions } from '@ionic/angular'; +``` + +## Docs for Overlays in Ionic + +For full docs and usage examples, visit the docs page for each of the overlays in Ionic: + +- [Action Sheet](https://ionicframework.com/docs/api/action-sheet) +- [Alert](https://ionicframework.com/docs/api/alert) +- [Loading](https://ionicframework.com/docs/api/loading) +- [Modal](https://ionicframework.com/docs/api/modal) +- [Picker](https://ionicframework.com/docs/api/picker) +- [Popover](https://ionicframework.com/docs/api/popover) +- [Toast](https://ionicframework.com/docs/api/toast) diff --git a/docs/angular/overview.md b/docs/angular/overview.md new file mode 100644 index 00000000000..7cf13ffcdb8 --- /dev/null +++ b/docs/angular/overview.md @@ -0,0 +1,71 @@ +--- +title: 'Ionic Angular Overview' +sidebar_label: Overview +--- + + + Ionic Angular Overview | Angular Version Support and Tooling + + + +import DocsCard from '@components/global/DocsCard'; +import DocsCards from '@components/global/DocsCards'; + +`@ionic/angular` brings the full power of the Ionic Framework to Angular developers. It offers seamless integration with the Angular ecosystem, so you can build high-quality cross-platform apps using familiar Angular tools, components, and best practices. You also get access to Ionic's extensive UI library and native capabilities. + +## Angular Version Support + +Ionic Angular v9 supports Angular versions 18 through 22. For detailed information on supported versions and our support policy, refer to the [Ionic Angular Support Policy](/docs/reference/support#ionic-angular). + +## Angular Tooling + +Ionic uses the official Angular stack for building apps and routing, so your app can fall in line with the rest of the Angular ecosystem. In cases where more opinionated features are needed, Ionic provides `@ionic/angular-toolkit`, which builds and integrates with the [official Angular CLI](https://angular.io/cli) and provides features that are specific to `@ionic/angular` apps. + +## Native Tooling + +[Capacitor](https://capacitorjs.com) is the official cross-platform runtime for Ionic Angular, enabling your apps to run natively on iOS, Android, and the web with a single codebase. + +## Installation + +Before you begin, make sure you have [Node.js](https://nodejs.org/) (which includes npm) installed on your machine. + +```shell-session +$ npm install -g @ionic/cli +$ ionic start myApp tabs --type angular + +$ cd myApp +$ ionic serve █ +``` + +## Resources + + + + +

Quickly set up your first Ionic Angular app and learn the basics of the framework and CLI.

+
+ + +

Learn more about Angular's core concepts, tools, and best practices from the official Angular documentation.

+
+ + +

Discover how to handle routing and navigation in Ionic Angular apps using the Angular Router.

+
+ + +

Explore Ionic's rich library of UI components for building beautiful apps.

+
+ + +

Learn how to customize the look and feel of your app with Ionic's powerful theming system.

+
+ + +

Explore how to access native device features and deploy your app to iOS, Android, and the web with Capacitor.

+
+ +
diff --git a/docs/angular/performance.md b/docs/angular/performance.md new file mode 100644 index 00000000000..de83b3af578 --- /dev/null +++ b/docs/angular/performance.md @@ -0,0 +1,73 @@ +--- +title: Angular Performance +sidebar_label: Performance +--- + + + Angular Performance: Manage App Component Change with ngFor + + + +## \*ngFor with Ionic Components + +When using `*ngFor` with Ionic components, we recommend using Angular's `trackBy` option. This allows Angular to manage change propagation in a much more efficient way and only update the content inside of the component rather than re-create the component altogether. + +By using `trackBy` you can provide a stable identity for each loop element so Angular can track insertions and deletions within the iterator. Below is an example of how to use `trackBy`: + +**home.page.html** + +```html + + {{ item.value }} + +``` + +**home.component.ts** + +```tsx + +items = [ + { id: 0, value: 'Item 0' }, + { id: 1, value: 'Item 1' }, + ... +] + +trackItems(index: number, itemObject: any) { + return itemObject.id; +} +``` + +In this example, we have an array of objects called `items`. Each object contains a `value` and an `id`. Using `trackBy`, we pass a `trackItems` function which returns the `id` of each object. This `id` is used to provide a stable identity for each loop element. + +For more information, refer to the [Angular NgForOf change propagation documentation](https://angular.io/api/common/NgForOf#change-propagation). + +## From the Ionic Team + +[How to Lazy Load in Ionic Angular](https://ionicframework.com/blog/how-to-lazy-load-in-ionic-angular/) + +[Improved Perceived Performance with Skeleton Screens](https://ionicframework.com/blog/improved-perceived-performance-with-skeleton-screens/) + +## From the Angular Team + +[Build performant and progressive Angular apps](https://web.dev/angular) - web.dev + +## From the Community + +{/* cspell:disable */} + +[High Performance Animations in Ionic](https://www.joshmorony.com/high-performance-animations-in-ionic/) - Josh Morony + +[High Performance List Filtering in Ionic](https://www.joshmorony.com/high-performance-list-filtering-in-ionic-2/) - Josh Morony + +[Increasing Performance with Efficient DOM Writes in Ionic](https://www.joshmorony.com/increasing-performance-with-efficient-dom-writes-in-ionic-2/) - Josh Morony + +[Ionic Framework is Fast (But Your Code Might Not Be)](https://www.joshmorony.com/ionic-framework-is-fast-but-your-code-might-not-be/) - Josh Morony + +{/* cspell:enable */} + +:::note +Do you have a guide you'd like to share? Click the _Edit this page_ button below. +::: diff --git a/docs/angular/platform.md b/docs/angular/platform.md new file mode 100644 index 00000000000..7aa72bba712 --- /dev/null +++ b/docs/angular/platform.md @@ -0,0 +1,287 @@ +--- +title: Platform +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + Platform | Ionic Platform to Customize Apps to Fit Any Device + + + +The Platform service can be used to get information about your current device. You can get all of the platforms associated with the device using the `platforms` method, including whether the app is being viewed from a tablet, if it's on a mobile device or browser, and the exact platform (iOS, Android, etc). You can also get the orientation of the device, if it uses right-to-left language direction, and much much more. With this information you can completely customize your app to fit any device. + +## Usage + + + + +```tsx +import { Platform } from '@ionic/angular/lazy'; + +@Component({...}) +export class MyPage { + constructor(public platform: Platform) { + + } +} +``` + + + + +```tsx +import { Platform } from '@ionic/angular'; + +@Component({...}) +export class MyPage { + constructor(public platform: Platform) { + + } +} +``` + + + + +## Methods + +### `is` + +| | | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | Depending on the platform the user is on, `is(platformName)` will return true or false. Note that the same app can return true for more than one platform name. For example, an app running from an iPad would return true for the platform names: `mobile`, `ios`, `ipad`, and `tablet`. Additionally, if the app was running from Cordova then `cordova` would be true. | +| **Signature** | `is(platformName: Platforms) => boolean` | + +#### Parameters + +| Name | Type | Description | +| -------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `platformName` | `Platforms` | Name of the platform. Available options are android, capacitor, cordova, desktop, electron, hybrid, ios, ipad, iphone, mobile, phablet, pwa, tablet | + +#### Platforms + +Below is a table listing all the possible platform values along with corresponding descriptions. + +| Platform Name | Description | +| ------------- | ---------------------------------------- | +| android | a device running Android | +| capacitor | a device running Capacitor | +| cordova | a device running Cordova | +| desktop | a desktop device | +| electron | a desktop device running Electron | +| hybrid | a device running Capacitor or Cordova | +| ios | a device running iOS | +| ipad | an iPad device | +| iphone | an iPhone device | +| mobile | a mobile device | +| mobileweb | a web browser running in a mobile device | +| phablet | a phablet device | +| pwa | a PWA app | +| tablet | a tablet device | + +#### Customizing Platform Detection Functions + +The function used to detect a specific platform can be overridden by providing an alternative function in the global [Ionic config](../developing/config). Each function takes `window` as a parameter and returns a boolean. + + + + +```tsx title="app.module.ts" +/* + * IonicModule is deprecated and will be removed in a future major version. + * Refer to the "Angular (Standalone)" tab to use `provideIonicAngular()` instead. + */ +import { IonicModule } from '@ionic/angular/lazy'; + +@NgModule({ + ... + imports: [ + BrowserModule, + IonicModule.forRoot({ + platform: { + /** The default `desktop` function returns false for devices with a touchscreen. + * This is not always wanted, so this function tests the User Agent instead. + **/ + 'desktop': (win) => { + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(win.navigator.userAgent); + return !isMobile; + } + }, + }), + AppRoutingModule + ], + ... +}) +``` + + + + +```ts title="main.ts" +import { provideIonicAngular } from '@ionic/angular'; + +bootstrapApplication(AppComponent, { + providers: [ + ..., + provideIonicAngular({ + platform: { + /** The default `desktop` function returns false for devices with a touchscreen. + * This is not always wanted, so this function tests the User Agent instead. + **/ + 'desktop': (win) => { + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(win.navigator.userAgent); + return !isMobile; + } + }, + }) + ] +}) +``` + + + + +```ts +type PlatformConfig = { + android?: ((win: Window) => boolean) | undefined; + capacitor?: ((win: Window) => boolean) | undefined; + cordova?: ((win: Window) => boolean) | undefined; + desktop?: ((win: Window) => boolean) | undefined; + electron?: ((win: Window) => boolean) | undefined; + hybrid?: ((win: Window) => boolean) | undefined; + ios?: ((win: Window) => boolean) | undefined; + ipad?: ((win: Window) => boolean) | undefined; + iphone?: ((win: Window) => boolean) | undefined; + mobile?: ((win: Window) => boolean) | undefined; + mobileweb?: ((win: Window) => boolean) | undefined; + phablet?: ((win: Window) => boolean) | undefined; + pwa?: ((win: Window) => boolean) | undefined; + tablet?: ((win: Window) => boolean) | undefined; +}; +``` + +### `platforms` + +| | | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | Depending on what device you are on, `platforms` can return multiple values. Each possible value is a hierarchy of platforms. For example, on an iPhone, it would return `mobile`, `ios`, and `iphone`. | +| **Signature** | `platforms() => string[]` | + +### `ready` + +| | | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | Returns a promise when the platform is ready and native functionality can be called. If the app is running from within a web browser, then the promise will resolve when the DOM is ready. When the app is running from an application engine such as Cordova, then the promise will resolve when Cordova triggers the `deviceready` event. The resolved value is the `readySource`, which states the platform that was used.

For example, when Cordova is ready, the resolved ready source is `cordova`. The default ready source value will be `dom`. The `readySource` is useful if different logic should run depending on the platform the app is running from. For example, only Capacitor and Cordova can execute the status bar plugin, so the web should not run status bar plugin logic. | +| **Signature** | `ready() => Promise` | + +### `isRTL` + +| | | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Description** | Returns if this app is using right-to-left language direction or not. We recommend the app's `index.html` file already has the correct `dir` attribute value set, such as `` or ``. [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir) | +| **Signature** | `isRTL() => boolean` | + +### `isLandscape` + +| | | +| --------------- | ----------------------------------------------- | +| **Description** | Returns `true` if the app is in landscape mode. | +| **Signature** | `isLandscape() => boolean` | + +### `isPortrait` + +| | | +| --------------- | ---------------------------------------------- | +| **Description** | Returns `true` if the app is in portrait mode. | +| **Signature** | `isPortrait() => boolean` | + +### `width` + +| | | +| --------------- | -------------------------------------------------------------------- | +| **Description** | Gets the width of the platform's viewport using `window.innerWidth`. | +| **Signature** | `width() => number` | + +### `height` + +| | | +| --------------- | ---------------------------------------------------------------------- | +| **Description** | Gets the height of the platform's viewport using `window.innerHeight`. | +| **Signature** | `height() => number` | + +### `url` + +| | | +| --------------- | -------------------- | +| **Description** | Get the current url. | +| **Signature** | `url() => string` | + +### `testUserAgent` + +| | | +| --------------- | ---------------------------------------------------------------------- | +| **Description** | Returns `true` if the expression is included in the user agent string. | +| **Signature** | `testUserAgent(expression: string) => boolean` | + +#### Parameters + +| Name | Type | Description | +| ---------- | ------ | ------------------------------------- | +| expression | string | The string to check in the user agent | + +## Events + +### `pause` + +The `pause` event emits when the native platform puts the application into the background, typically when the user switches to a different application. This event emits when a Cordova/Capacitor app is put into the background but doesn't fire in a standard web browser. + +#### Examples + +```tsx +this.platform.pause.subscribe(async () => { + alert('Pause event detected'); +}); +``` + +### `resize` + +The `resize` event emits when the browser window has changed dimensions. This could be from a browser window being physically resized, or from a device changing orientation. + +#### Examples + +```tsx +this.platform.resize.subscribe(async () => { + alert('Resize event detected'); +}); +``` + +### `resume` + +The `resume` event fires when the native platform pulls the application out from the background. This event emits when a Cordova/Capacitor app comes out from the background but doesn't fire in a standard web browser. + +#### Examples + +```tsx +this.platform.resume.subscribe(async () => { + alert('Resume event detected'); +}); +``` diff --git a/docs/angular/pwa.md b/docs/angular/pwa.md new file mode 100644 index 00000000000..47666c70309 --- /dev/null +++ b/docs/angular/pwa.md @@ -0,0 +1,166 @@ +--- +title: Progressive Web Apps in Angular +sidebar_label: Progressive Web Apps +--- + + + Make Progressive Web Apps (PWA) in Angular - Ionic Tutorial + + + +## Making your Angular app a PWA + +The two main requirements of a PWA are a
Service Worker and a Web Manifest. While it's possible to add both of these to an app manually, the Angular team has an `@angular/pwa` package that can be used to automate this. + +The `@angular/pwa` package will automatically add a service worker and an app manifest to the app. +To add this package to the app, run: + +```shell +ng add @angular/pwa +``` + +Once this package has been added run `ionic build --prod` and the `www` directory will be ready to deploy as a PWA. + +:::note +By default, the `@angular/pwa` package comes with the Angular logo for the app icons. Be sure to update the manifest to use the correct app name and also replace the icons. +::: + +:::note +Features like Service Workers and many JavaScript APIs (such as geolocation) require the app be hosted in a secure context. When deploying an app through a hosting service, be aware that HTTPS will be required to take full advantage of Service Workers. +::: + +## Service Worker configuration + +After `@angular/pwa` has been added, a new `ngsw-config.json` file will be created at the root of the project. This file is responsible for configuring how Angular's service worker mechanism will handle caching assets. By default, the following will be provided: + +```json +{ + "$schema": "./node_modules/@angular/service-worker/config/schema.json", + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] + } + } + ] +} +``` + +There are two sections in here, one for app specific resources (JS, CSS, HTML) and assets the app will load on demand. Depending on your app, these options can be customized. For a more detailed guide, read [the official guide from the Angular Team](https://angular.io/guide/service-worker-config). + +## Deploying + +### Firebase + +Firebase hosting provides many benefits for Progressive Web Apps, including fast response times thanks to CDNs, HTTPS enabled by default, and support for [HTTP2 push](https://firebase.googleblog.com/2016/09/http2-comes-to-firebase-hosting.html). + +First, if not already available, [create the project](https://console.firebase.google.com) in Firebase. + +Next, in a Terminal, install the Firebase CLI: + +```shell +npm install -g firebase-tools +``` + +:::note +If it's the first time you use firebase-tools, login to your Google account with `firebase login` command. +::: + +With the Firebase CLI installed, run `firebase init` within your Ionic project. The CLI prompts: + +**"Which Firebase CLI features do you want to set up for this folder?"** Choose "Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys". + +Create a new Firebase project or select an existing one. + +**"Select a default Firebase project for this directory:"** Choose the project you created on the Firebase website. + +**"What do you want to use as your public directory?"** Enter "www". + +:::note +Answering this next question will ensure that routing, hard reload, and deep linking work in the app: +::: + +**Configure as a single-page app (rewrite all urls to /index.html)?"** Enter "Yes". + +**"File build/index.html already exists. Overwrite?"** Enter "No". + +**Set up automatic builds and deploys with Github?** Enter "Yes". + +**For which GitHub repository would you like to set up a Github Workflow?** Enter your project name. + +**Set up the workflow to run a build script before every deploy?** Enter "Yes". + +**What script should be run before every deploy?** Enter `npm ci && npm run build`. + +**Set up automatic deployment to your sites live channel when a PR is merged?** Enter "Yes". + +**What is the name of the get hooked branch associated with your sites live channel?** Enter your project's main branch name. + +A `firebase.json` config file is generated, configuring the app for deployment. + +The last thing needed is to make sure caching headers are being set correctly. To do this, add a `headers` snippet to the `firebase.json` file. The complete `firebase.json` looks like: + +```json +{ + "hosting": { + "public": "www", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ], + "headers": [ + { + "source": "/build/app/**", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=31536000" + } + ] + }, + { + "source": "ngsw-worker.js", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache" + } + ] + } + ] + } +} +``` + +For more information about the `firebase.json` properties, refer to the [Firebase documentation](https://firebase.google.com/docs/hosting/full-config#section-firebase-json). + +Next, build an optimized version of the app by running: + +```shell +ionic build --prod +``` + +Last, deploy the app by running: + +```shell +firebase deploy +``` + +After this completes, the app will be live. diff --git a/docs/angular/quickstart.md b/docs/angular/quickstart.md new file mode 100644 index 00000000000..e2c74442b9b --- /dev/null +++ b/docs/angular/quickstart.md @@ -0,0 +1,441 @@ +--- +title: Ionic Angular Quickstart +sidebar_label: Quickstart +--- + + + Ionic Angular Quickstart Using Ionic CLI: Angular Basics + + + +import DocsCard from '@components/global/DocsCard'; +import DocsCards from '@components/global/DocsCards'; + +Welcome! This guide will walk you through the basics of Ionic Angular development. You'll learn how to set up your development environment, generate a simple project, explore the project structure, and understand how Ionic components work. This is perfect for getting familiar with Ionic Angular before building your first real app. + +If you're looking for a high-level overview of what Ionic Angular is and how it fits into the Angular ecosystem, refer to the [Ionic Angular Overview](overview). + +## Prerequisites + +Before you begin, make sure you have Node.js and npm installed on your machine. +You can check by running: + +```shell +node -v +npm -v +``` + +If you don't have Node.js and npm, [download Node.js](https://nodejs.org/en/download) (which includes npm). + +## Create a Project with the Ionic CLI + +First, install the latest [Ionic CLI](../cli): + +```shell +npm install -g @ionic/cli +``` + +Then, run the following commands to create and run a new project: + +```shell +ionic start myApp blank --type angular + +cd myApp +ionic serve +``` + +At the first prompt, choose `Standalone`. + +After running `ionic serve`, your project will open in the browser. + +![Screenshot of the Ionic Angular Home page](/img/guides/quickstart/home-page.png 'Ionic Angular Home Component') + +## Explore the Project Structure + +Your new app's directory will look like this: + +```shell +└── src/ + └── app + ├── app.component.html + ├── app.component.scss + ├── app.component.ts + ├── app.routes.ts + └── home/ + ├── home.page.html + ├── home.page.scss + ├── home.page.spec.ts + └── home.page.ts +``` + +:::info +All file paths in the examples below are relative to the project root directory. +::: + +Let's walk through these files to understand the app's structure. + +## View the App Component + +The root of your app is defined in `app.component.ts`: + +```ts title="src/app/app.component.ts" +import { Component } from '@angular/core'; +import { IonApp, IonRouterOutlet } from '@ionic/angular'; + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + imports: [IonApp, IonRouterOutlet], +}) +export class AppComponent { + constructor() {} +} +``` + +And its template in `app.component.html`: + +```html title="src/app/app.component.html" + + + +``` + +This sets up the root of your application, using Ionic's `ion-app` and `ion-router-outlet` components. The router outlet is where your pages will be displayed. + +## View Routes + +Routes are defined in `app.routes.ts`: + +```ts title="src/app/app.routes.ts" +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: 'home', + loadComponent: () => import('./home/home.page').then((m) => m.HomePage), + }, + { + path: '', + redirectTo: 'home', + pathMatch: 'full', + }, +]; +``` + +When you visit the root URL (`/`), the `HomePage` component will be loaded. + +## View the Home Page + +The Home page component, defined in `home.page.ts`, imports the Ionic components it uses: + +```ts title="src/app/home/home.page.ts" +import { Component } from '@angular/core'; +import { IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/angular'; + +@Component({ + selector: 'app-home', + templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], + imports: [IonHeader, IonToolbar, IonTitle, IonContent], +}) +export class HomePage { + constructor() {} +} +``` + +And the template, in the `home.page.html` file, uses those components: + +```html title="src/app/home/home.page.html" + + + Blank + + + + + + + Blank + + + +
+ Ready to create an app? +

+ Start with Ionic + UI Components +

+
+
+``` + +This creates a page with a header and scrollable content area. The second header shows a [collapsible large title](/docs/api/title.md#collapsible-large-titles) that displays on iOS devices when at the top of the content, then condenses to show the smaller title in the first header when scrolling down. + +:::tip[Learn More] +For detailed information about Ionic layout components, refer to the [Header](/docs/api/header.md), [Toolbar](/docs/api/toolbar.md), [Title](/docs/api/title.md), and [Content](/docs/api/content.md) documentation. +::: + +## Add an Ionic Component + +You can enhance your Home page with more Ionic UI components. For example, add a [Button](/docs/api/button.md) at the end of the `ion-content`: + +```html title="src/app/home/home.page.html" + + + + Navigate + +``` + +Then, import the `IonButton` component in `home.page.ts`: + +```ts title="src/app/home/home.page.ts" +import { IonButton, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + // ...existing config... + imports: [IonButton, IonContent, IonHeader, IonTitle, IonToolbar], +}) +``` + +## Add a New Page + +To add a new page, generate it with the CLI: + +```shell +ionic generate page new +``` + +A route will be automatically added to `app.routes.ts`. + +In `new.page.html`, you can add a [Back Button](/docs/api/back-button.md) to the [Toolbar](/docs/api/toolbar.md): + +```html title="src/app/new/new.page.html" + + + + + + new + + +``` + +And import `IonBackButton` and `IonButtons` in `new.page.ts`: + +```ts title="src/app/new/new.page.ts" +import { IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar } from '@ionic/angular'; + +@Component({ + // ...existing config... + imports: [IonBackButton, IonButtons, IonContent, IonHeader, IonTitle, IonToolbar], +}) +``` + +The `ion-back-button` will automatically handle navigation back to the previous page, or to `/` if there is no history. + +## Navigate to the New Page + +To navigate to the new page, update the button in `home.page.html`: + +```html title="src/app/home/home.page.html" +Navigate +``` + +Then, import `RouterLink` in `home.page.ts`: + +```ts title="src/app/home/home.page.ts" +import { RouterLink } from '@angular/router'; + +@Component({ + // ...existing config... + imports: [IonButton, IonContent, IonHeader, IonTitle, IonToolbar, RouterLink], +}) +``` + +:::info +Navigating can also be performed using Angular's Router service. Refer to the [Angular Navigation documentation](/docs/angular/navigation.md#navigating-to-different-routes) for more information. +::: + +## Add Icons to the New Page + +Ionic Angular comes with [Ionicons](https://ionic.io/ionicons/) pre-installed. You can use any icon by setting the `name` property on the `ion-icon` component. Add the following icons to `new.page.html`: + +```html title="src/app/new/new.page.html" + + + + + + +``` + +You'll also need to import and register these icons in `new.page.ts`: + +```ts title="src/app/new/new.page.ts" +// ...existing imports... +import { IonBackButton, IonButtons, IonContent, IonHeader, IonIcon, IonTitle, IonToolbar } from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { heart, logoIonic } from 'ionicons/icons'; + +@Component({ + // ...existing config... + imports: [IonBackButton, IonButtons, IonContent, IonHeader, IonIcon, IonTitle, IonToolbar], +}) +``` + +Then, update the constructor of the page to use `addIcons`: + +```ts title="src/app/new/new.page.ts" +export class NewPage implements OnInit { + constructor() { + addIcons({ heart, logoIonic }); + } + + ngOnInit() {} +} +``` + +Alternatively, you can register icons in `app.component.ts` to use them throughout your app. + +For more information, refer to the [Icon documentation](/docs/api/icon.md) and the [Ionicons documentation](https://ionic.io/ionicons/). + +## Call Component Methods + +Let's add a button that can scroll the content area to the bottom. + +Update the `ion-content` in your `new.page.html` to include a button and some items after the existing icons: + +```html title="src/app/new/new.page.html" + + + + new + + + + + + + Scroll to Bottom + + + @for (item of items; track $index; let i = $index) { + + Item {{ i + 1 }} + + } + +``` + +In the component, add the `ViewChild` import, the new component imports and define the `scrollToBottom` function: + +```ts title="src/app/new/new.page.ts" +import { Component, OnInit, ViewChild } from '@angular/core'; +import { + IonBackButton, + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonTitle, + IonToolbar, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { heart, logoIonic } from 'ionicons/icons'; + +@Component({ + // ...existing config... + imports: [ + IonBackButton, + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonTitle, + IonToolbar, + ], +}) +export class NewPage implements OnInit { + @ViewChild(IonContent) content!: IonContent; + + items = Array.from({ length: 50 }, (_, i) => i); + + constructor() { + addIcons({ heart, logoIonic }); + } + + ngOnInit() {} + + scrollToBottom = () => { + this.content.scrollToBottom(300); + }; +} +``` + +To call methods on Ionic components: + +1. Create a `ViewChild` reference for the component +2. Call the method directly on the component instance + +You can find available methods for each component in the [Methods](/docs/api/content.md#methods) section of their API documentation. + +## Run on a Device + +Ionic's components work everywhere: on iOS, Android, and PWAs. To deploy to mobile, use [Capacitor](https://capacitorjs.com): + +```shell +ionic build +ionic cap add ios +ionic cap add android +``` + +Open the native projects in their IDEs: + +```shell +ionic cap open ios +ionic cap open android +``` + +Refer to [Capacitor's Getting Started guide](https://capacitorjs.com/docs/getting-started/with-ionic) for more. + +## Explore More + +This guide covered the basics of creating an Ionic Angular app, adding navigation, and introducing Capacitor for native builds. To dive deeper, check out: + + + + +

Build a real Photo Gallery app with Ionic Angular and native device features.

+
+ + +

Learn more about Angular's core concepts, tools, and best practices from the official Angular documentation.

+
+ + +

Discover how to handle routing and navigation in Ionic Angular apps using the Angular Router.

+
+ + +

Explore Ionic's rich library of UI components for building beautiful apps.

+
+ + +

Learn how to customize the look and feel of your app with Ionic's powerful theming system.

+
+ + +

Explore how to access native device features and deploy your app to iOS, Android, and the web with Capacitor.

+
+ +
diff --git a/docs/angular/slides.md b/docs/angular/slides.md new file mode 100644 index 00000000000..efb46441f4b --- /dev/null +++ b/docs/angular/slides.md @@ -0,0 +1,366 @@ +--- +title: Migrating from ion-slides to Swiper.js +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + Set Up Swiper.js for Angular Slides [Example] | Ionic + + + +:::warning[Looking for `ion-slides`?] +`ion-slides` was deprecated in v6.0.0 and removed in v7.0.0. We recommend using the Swiper.js library directly. The migration process is detailed below. +::: + +We recommend Swiper.js if you need a modern touch slider component. Swiper 9 introduced Swiper Element as a replacement for its Angular component, so this guide will go over how to get Swiper Element set up in your Ionic Framework application. It will also go over any migration information you may need to move from `ion-slides` to Swiper Element. + +## Getting Started + +First, update to the latest version of Ionic: + +```shell +npm install @ionic/angular@latest +``` + +Once that is done, install the Swiper dependency in your project: + +```shell +npm install swiper@latest +``` + +Next, we need to add the `CUSTOM_ELEMENTS_SCHEMA`, which tells Angular that we will be using custom elements. This can be done in either `app.module.ts`, or the module file for the component where you will be using Swiper. + +```typescript +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + +@NgModule({ + schemas: [..., CUSTOM_ELEMENTS_SCHEMA] +}); +... +``` + +Finally, we need to call Swiper's `register` function to globally register Swiper's custom elements. This should only be done once, so place it in `app.component.ts`. + +```typescript +import { register } from 'swiper/element/bundle'; + +register(); + +@Component({ + ... +}) +... +``` + +From there, we just have to replace `ion-slides` elements with `swiper-container` and `ion-slide` elements with `swiper-slide`. Note that these custom elements do not need to be imported, as calling `register` tells Angular about them on its own. + +```html + + Slide 1 + Slide 2 + Slide 3 + +``` + +## Bundled vs. Core Versions + +By default, make sure you import the `register` function from `swiper/element/bundle`. This uses the bundled version of Swiper, which automatically includes all modules and stylesheets needed to run Swiper's various features. + +If you would like to use the Core version instead, which does not include additional modules automatically, refer to Swiper's core version and modules documentation. The rest of this migration guide will assume you are using the bundled version. + +## Swiping with Style + +To migrate over your CSS, first update your selectors to target the new custom elements instead: + +| ion-slides Selector | Swiper Selector | +| ------------------- | ------------------ | +| `ion-slides` | `swiper-container` | +| `ion-slide` | `swiper-slide` | + +If you were using the CSS custom properties found on `ion-slides`, below is a list of corresponding properties used in Swiper. + +| `ion-slides` CSS property | `swiper-container` CSS property | +| ---------------------------------- | ------------------------------------------- | +| `--bullet-background` | `--swiper-pagination-bullet-inactive-color` | +| `--bullet-background-active` | `--swiper-pagination-color` | +| `--progress-bar-background` | `--swiper-pagination-progressbar-bg-color` | +| `--progress-bar-background-active` | `--swiper-pagination-color` | +| `--scroll-bar-background` | `--swiper-scrollbar-bg-color` | +| `--scroll-bar-background-active` | `--swiper-scrollbar-drag-bg-color` | + +For additional custom CSS, because Swiper Element uses Shadow DOM encapsulation, styles will need to be injected into the Shadow DOM scope. Refer to Swiper's guide on injecting styles for instructions. + +### Additional `ion-slides` Styles + +The `ion-slides` component had additional styling that helped create a native look and feel. These styles are **not** required to use Swiper.js with Ionic, but if you would like to maintain the look of `ion-slides` as closely as possible, add the following CSS to your `global.scss`: + +```css +swiper-container { + --swiper-pagination-bullet-inactive-color: var(--ion-text-color-step-800, #cccccc); + --swiper-pagination-color: var(--ion-color-primary, #0054e9); + --swiper-pagination-progressbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.25); + --swiper-scrollbar-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.1); + --swiper-scrollbar-drag-bg-color: rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.5); +} + +swiper-slide { + display: flex; + position: relative; + + flex-direction: column; + flex-shrink: 0; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; + + font-size: 18px; + + text-align: center; + box-sizing: border-box; +} + +swiper-slide img { + width: auto; + max-width: 100%; + height: auto; + max-height: 100%; +} +``` + +## The IonicSlides Module + +With `ion-slides`, Ionic automatically customized dozens of Swiper properties. This resulted in an experience that felt smooth when swiping on mobile devices. We recommend using the `IonicSlides` module to ensure that these properties are also set when using Swiper directly. However, using this module is **not** required to use Swiper.js in Ionic. + +It is recommended to review the [properties](https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/slides/IonicSlides.ts) set by `IonicSlides` and determine which ones you would like to customize. + +We can install the `IonicSlides` module by importing and passing it to the `modules` property of `swiper-container` as an array: + + + + +```typescript +// home.page.ts + +import { IonicSlides } from '@ionic/angular/lazy'; + +@Component({ + ... +}) +export class HomePage { + swiperModules = [IonicSlides]; +} +``` + + + + +```typescript +// home.page.ts + +import { IonicSlides } from '@ionic/angular'; + +@Component({ + ... +}) +export class HomePage { + swiperModules = [IonicSlides]; +} +``` + + + + +```html + + + ... +``` + +:::note +If you are using the Core version of Swiper and have installed additional modules, ensure that `IonicSlides` is the last module in the array. This will let it automatically customize the settings of modules such as Pagination, Scrollbar, Zoom, and more. +::: + +## Properties + +Swiper options should be provided as individual properties directly on the `` component. + +Let's say in an app with `ion-slides` we had the `slidesPerView` and `loop` options set: + +```html + + Slide 1 + Slide 3 + Slide 3 + +``` + +To set these options as properties directly on `` we would do the following: + +```html + + Slide 1 + Slide 2 + Slide 3 + +``` + +Below is a full list of property changes when going from `ion-slides` to Swiper Element: + +| Name | Notes | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| options | Set each option as a property directly on the `` component. | +| mode | For different styles based upon the mode, you can target the slides with `.ios swiper-container` or `.md swiper-container` in your CSS. | +| pager | Use the `pagination` property instead. | + +:::note +All properties available in Swiper Element can be found in the Swiper API parameters documentation. +::: + +## Events + +Since the `swiper-container` component is not provided by Ionic Framework, event names will not have an `ionSlide` prefix to them. Additionally, all event names should be lowercase instead of camelCase. + +Let's say in an app with `ion-slides` we used the `ionSlideDidChange` event: + +```html + + Slide 1 + Slide 3 + Slide 3 + +``` + +To migrate, we would change the name of the event to `swiperslidechange`: + +```html + + Slide 1 + Slide 2 + Slide 3 + +``` + +Below is a full list of event name changes when going from `ion-slides` to Swiper Angular: + +| ion-slides Event | Swiper Event | +| ------------------------- | ---------------------------------- | +| `ionSlideWillChange` | `swiperslidechangetransitionstart` | +| `ionSlideDidChange` | `swiperslidechange` | +| `ionSlideDoubleTap` | `swiperdoubletap` | +| `ionSlideDrag` | `swiperslidermove` | +| `ionSlideNextStart` | `swiperslidenexttransitionstart` | +| `ionSlideNextEnd` | `swiperslidenexttransitionend` | +| `ionSlidePrevStart` | `swiperslideprevtransitionstart` | +| `ionSlidePrevEnd` | `swiperslideprevtransitionend` | +| `ionSlideReachStart` | `swiperreachbeginning` | +| `ionSlideReachEnd` | `swiperreachend` | +| `ionSlideTap` | `swipertap` | +| `ionSlideTouchStart` | `swipertouchstart` | +| `ionSlideTouchEnd` | `swipertouchend` | +| `ionSlideTransitionStart` | `swipertransitionstart` | +| `ionSlideTransitionEnd` | `swipertransitionend` | +| `ionSlidesDidLoad` | `swiperinit` | + +:::note +All events available in Swiper Element can be found in the Swiper API events documentation and should be lowercased and prefixed with the word `swiper`. +::: + +## Methods + +Most methods have been removed in favor of directly accessing the properties of the Swiper instance. To access the Swiper instance, first get a reference to the `` element (such as through `ViewChild`), then access its `swiper` prop: + +```html + + + + Slide 1 + Slide 2 + Slide 3 + +``` + +```typescript +// slides.component.ts + +import { ..., ElementRef, ViewChild } from '@angular/core'; + +@Component({ + ... +}) +export class SlidesExample { + @ViewChild('swiper') + swiperRef: ElementRef | undefined; + + logActiveIndex() { + console.log(this.swiperRef?.nativeElement.swiper.activeIndex); + } +} +``` + +Below is a full list of method changes when going from `ion-slides` to Swiper Element: + +| ion-slides Method | Notes | +| -------------------- | ------------------------------------------------------------------------------------ | +| `getActiveIndex()` | Use the `activeIndex` property instead. | +| `getPreviousIndex()` | Use the `previousIndex` property instead. | +| `getSwiper()` | Get a reference to the Swiper instance using the `swiper` prop. See example above. | +| `isBeginning()` | Use the `isBeginning` property instead. | +| `isEnd()` | Use the `isEnd` property instead. | +| `length()` | Use the `slides` property instead. (i.e swiper.slides.length) | +| `lockSwipeToNext()` | Use the `allowSlidesNext` property instead. | +| `lockSwipeToPrev()` | Use the `allowSlidePrev` property instead. | +| `lockSwipes()` | Use the `allowSlideNext`, `allowSlidePrev`, and `allowTouchMove` properties instead. | +| `startAutoplay()` | Use the `autoplay` property instead. | +| `stopAutoplay()` | Use the `autoplay` property instead. | + +:::note +All methods and properties available on the Swiper instance can be found in the Swiper API methods and properties documentation. +::: + +## Effects + +Effects such as Cube or Fade can be used in Swiper Element with no additional imports, as long as you are using the bundled version of Swiper. For example, the below code will cause the slides to have a flip transition effect: + +```html + ... +``` + +:::note +For more information on effects in Swiper, please refer to the Swiper API fade effect documentation. +::: + +## Wrap Up + +Now that you have Swiper installed, there is a whole set of new Swiper features for you to enjoy. We recommend starting with the Swiper Element documentation and then referencing the Swiper API docs. + +## FAQ + +### Where can I find an example of this migration? + +You can find a sample app with `ion-slides` and the equivalent Swiper usage at https://github.com/ionic-team/slides-migration-samples. + +### Where can I get help with this migration? + +If you are running into issues with the migration, please create a post on the [Ionic Forum](https://forum.ionicframework.com/). + +### Where do I file bug reports? + +Before opening an issue, please consider creating a post on the Swiper Discussion Board or the Ionic Forum to check if your issue can be resolved by the community. + +If you are running into problems with the Swiper library, new bugs should be filed on the Swiper issue tracker. + +If you are running into problems with the `IonicSlides` module, new bugs should be filed on the Ionic Framework issue tracker. diff --git a/docs/angular/storage.md b/docs/angular/storage.md new file mode 100644 index 00000000000..b7774589e80 --- /dev/null +++ b/docs/angular/storage.md @@ -0,0 +1,50 @@ +--- +title: Data Storage +sidebar_label: Storage +--- + + + Angular App Data Storage Options - Ionic Documentation + + + +There are a variety of options available for storing data within an Ionic application. It is best to choose options that best fit the needs of your application. A single application may have requirements that span multiple options. + +:::info +Some storage options involve third-party plugins or products. In such cases, we neither endorse nor support those plugins or products. We are mentioning them here for informational purposes only. +::: + +Here are some common use cases and solutions: + +## Local Application Settings and Data + +Many applications need to locally store settings as well as other lightweight key/value data. The [Capacitor Preferences](https://capacitorjs.com/docs/apis/preferences) plugin is specifically designed to handle these scenarios. + +## Relational Data Storage (Mobile Only) + +Some applications, especially those following an offline-first methodology, may require locally storing high volumes of complex relational data. For such scenarios, a SQLite plugin may be used. The most common SQLite plugin offerings are: + +- [Cordova SQLite Storage](https://github.com/storesafe/cordova-sqlite-storage) (a [convenience wrapper](https://danielsogl.gitbook.io/awesome-cordova-plugins/sqlite) also exists for this plugin to aid in implementation) +- [Capacitor Community SQLite Plugin](https://github.com/capacitor-community/sqlite) + +## Non-Relational High Volume Data Storage (Mobile and Web) + +For applications that need to store a high volume of data as well as operate on both web and mobile, a potential solution is to create a key/value pair data storage service that uses [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) on the web and one of the previously mentioned SQLite plugins on mobile. + +Here a sample of how this can be accomplished: + +- [Full application](https://github.com/ionic-enterprise/tutorials-and-demos-ng/tree/main/demos/sqlcipher-kv-pair) +- [Mobile Service](https://github.com/ionic-enterprise/tutorials-and-demos-ng/blob/main/demos/sqlcipher-kv-pair/src/app/core/mobile-kv-store.ts) +- [Web Service](https://github.com/ionic-enterprise/tutorials-and-demos-ng/blob/main/demos/sqlcipher-kv-pair/src/app/core/web-kv-store.ts) + +## Other Options + +Other storage options that provide local as well as cloud-based storage that work well within Capacitor applications also exist and may integrate well with your application. + +For example: + +- [PouchDB](https://pouchdb.com/) +- [Firestore](https://firebase.google.com/docs/firestore) diff --git a/docs/angular/testing.md b/docs/angular/testing.md new file mode 100644 index 00000000000..e11d91d4b04 --- /dev/null +++ b/docs/angular/testing.md @@ -0,0 +1,621 @@ +--- +title: Testing +--- + + + Angular Unit and End-to-End Testing for Ionic App Components + + + +When an `@ionic/angular` application is generated using the Ionic CLI, it is automatically set up for unit testing and end-to-end testing of the application. This is the same setup that is used by the Angular CLI. Refer to the Angular Testing Guide for detailed information on testing Angular applications. + +## Testing Principles + +When testing an application, it is best to keep in mind that testing can show if defects are present in a system. However, it is impossible to prove that any non-trivial system is completely free of defects. For this reason, the goal of testing is not to verify that the code is correct but to find problems within the code. This is a subtle but important distinction. + +If we set out to prove that the code is correct, we are more likely to stick to the happy path through the code. If we set out to find problems, we are more likely to more fully exercise the code and find the bugs that are lurking there. + +It is also best to begin testing an application from the very start. This allows defects to be found early in the process when they are easier to fix. This also allows code to be refactored with confidence as new features are added to the system. + +## Unit Testing + +Unit tests exercise a single unit of code (component, page, service, pipe, etc) in isolation from the rest of the system. Isolation is achieved through the injection of mock objects in place of the code's dependencies. The mock objects allow the test to have fine-grained control of the outputs of the dependencies. The mocks also allow the test to determine which dependencies have been called and what has been passed to them. + +Well-written unit tests are structured such that the unit of code and the features it contains are described via `describe()` callbacks. The requirements for the unit of code and its features are tested via `it()` callbacks. When the descriptions for the `describe()` and `it()` callbacks are read, they make sense as a phrase. When the descriptions for nested `describe()`s and a final `it()` are concatenated together, they form a sentence that fully describes the test case. + +Since unit tests exercise the code in isolation, they are fast, robust, and allow for a high degree of code coverage. + +### Using Mocks + +Unit tests exercise a code module in isolation. To facilitate this, we recommend using Jasmine (https://jasmine.github.io/). Jasmine creates mock objects (which Jasmine calls "spies") to take the place of dependencies while testing. When a mock object is used, the test can control the values returned by calls to that dependency, making the current test independent of changes made to the dependency. This also makes the test setup easier, allowing the test to only be concerned with the code within the module under test. + +Using mocks also allows the test to query the mock to determine if it was called and how it was called via the `toHaveBeenCalled*` set of functions. Tests should be as specific as possible with these functions, favoring calls to `toHaveBeenCalledTimes` over calls to `toHaveBeenCalled` when testing that a method has been called. That is `expect(mock.foo).toHaveBeenCalledTimes(1)` is better than `expect(mock.foo).toHaveBeenCalled()`. The opposite advice should be followed when testing that something has not been called (`expect(mock.foo).not.toHaveBeenCalled()`). + +There are two common ways to create mock objects in Jasmine. Mock objects can be constructed from scratch using `jasmine.createSpy` and `jasmine.createSpyObj` or spies can be installed onto existing objects using `spyOn()` and `spyOnProperty()`. + +#### Using `jasmine.createSpy` and `jasmine.createSpyObj` + +`jasmine.createSpyObj` creates a full mock object from scratch with a set of mock methods defined on creation. This is useful in that it is very simple. Nothing needs to be constructed or injected into the test. The disadvantage of using this function is that it allows the creation of objects that may not match the real objects. + +`jasmine.createSpy` is similar but it creates a stand-alone mock function. + +#### Using `spyOn()` and `spyOnProperty()` + +`spyOn()` installs the spy on an existing object. The advantage of using this technique is that if an attempt is made to spy on a method that does not exist on the object, an exception is raised. This prevents the test from mocking methods that do not exist. The disadvantage is that the test needs a fully formed object to begin with, which may increase the amount of test setup required. + +`spyOnProperty()` is similar with the difference being that it spies on a property and not a method. + +### General Testing Structure + +Unit tests are contained in `spec` files with one `spec` file per entity (component, page, service, pipe, etc.). The `spec` files live side-by-side with and are named after the source that they are testing. For example, if the project has a service called WeatherService, the code for it is in a file named `weather.service.ts` with the tests in a file named `weather.service.spec.ts`. Both of those files are in the same folder. + +The `spec` files themselves contain a single `describe` call that defines that overall test. Nested within it are other `describe` calls that define major areas of functionality. Each `describe` call can contain setup and teardown code (generally handled via `beforeEach` and `afterEach` calls), more `describe` calls forming a hierarchical breakdown of functionality, and `it` calls which define individual test cases. + +The `describe` and `it` calls also contain a descriptive text label. In well-formed tests, the `describe` and `it` calls combine with their labels to perform proper phrases and the full label for each test case, formed by combining the `describe` and `it` labels, creates a full sentence. + +For example: + +```tsx +describe('Calculation', () => { + describe('divide', () => { + it('calculates 4 / 2 properly' () => {}); + it('cowardly refuses to divide by zero' () => {}); + ... + }); + + describe('multiply', () => { + ... + }); +}); +``` + +The outer `describe` call states that the `Calculation` service is being tested, the inner `describe` calls state exactly what functionality is being tested, and the `it` calls state what the test cases are. When run the full label for each test case is a sentence that makes sense (Calculation divide cowardly refuses to divide by zero). + +### Pages and Components + +Pages are just Angular components. Thus, pages and components are both tested using Angular's Component Testing guidelines. + +Since pages and components contain both TypeScript code and HTML template markup it is possible to perform both component class testing and component DOM testing. When a page is created, the template test that is generated looks like this: + +```tsx +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsPage } from './tabs.page'; + +describe('TabsPage', () => { + let component: TabsPage; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TabsPage], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TabsPage); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); +``` + +When doing component class testing, the component object is accessed using the component object defined via `component = fixture.componentInstance;`. This is an instance of the component class. When doing DOM testing, the `fixture.nativeElement` property is used. This is the actual `HTMLElement` for the component, which allows the test to use standard HTML API methods such as `HTMLElement.querySelector` in order to examine the DOM. + +### Waiting for Components + +When testing Ionic components, use the `componentOnReady` helper exported from `@ionic/core` rather than calling `el.componentOnReady()` directly. The `el.componentOnReady()` method only exists on lazy-loaded elements and calling it directly throws an error on custom-element builds, which is what standalone projects use. The helper handles both. It awaits the element's own `componentOnReady()` promise when that exists. Otherwise it waits one animation frame, giving the component's inner contents a chance to render. Wait for the callback before asserting against the rendered DOM or running accessibility tests. + +```tsx +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { componentOnReady } from '@ionic/core'; +import { HomePage } from './home.page'; + +describe('HomePage', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomePage], + }).compileComponents(); + fixture = TestBed.createComponent(HomePage); + fixture.detectChanges(); + }); + + it('renders the submit button', async () => { + const button = fixture.nativeElement.querySelector('ion-button'); + await new Promise((resolve) => componentOnReady(button, () => resolve())); + expect(button.textContent).toContain('Submit'); + }); +}); +``` + +## Services + +Services often fall into one of two broad categories: utility services that perform calculations and other operations, and data services that perform primarily HTTP operations and data manipulation. + +### Basic Service Testing + +The suggested way to test most services is to instantiate the service and manually inject mocks for any dependency the service has. This way, the code can be tested in isolation. + +Let's say that there is a service with a method that takes an array of timecards and calculates net pay. Let's also assume that the tax calculations are handled via another service that the current service depends on. This payroll service could be tested as such: + +```tsx +import { PayrollService } from './payroll.service'; + +describe('PayrollService', () => { + let service: PayrollService; + let taxServiceSpy; + + beforeEach(() => { + taxServiceSpy = jasmine.createSpyObj('TaxService', { + federalIncomeTax: 0, + stateIncomeTax: 0, + socialSecurity: 0, + medicare: 0 + }); + service = new PayrollService(taxServiceSpy); + }); + + describe('net pay calculations', () => { + ... + }); +}); +``` + +This allows the test to control the values returned by the various tax calculations via mock setup such as `taxServiceSpy.federalIncomeTax.and.returnValue(73.24)`. This allows the "net pay" tests to be independent of the tax calculation logic. When the tax codes change, only the tax service related code and tests need to change. The tests for the net pay can continue to operate as they are since these tests do not care how the tax is calculated, just that the value is applied properly. + +The scaffolding that is used when a service is generated via `ionic g service name` uses Angular's testing utilities and sets up a testing module. Doing so is not strictly necessary. That code may be left in, however, allowing the service to be built manually or injected as such: + +```tsx +import { TestBed, inject } from '@angular/core/testing'; + +import { PayrollService } from './payroll.service'; +import { TaxService } from './tax.service'; + +describe('PayrolService', () => { + let taxServiceSpy; + + beforeEach(() => { + taxServiceSpy = jasmine.createSpyObj('TaxService', { + federalIncomeTax: 0, + stateIncomeTax: 0, + socialSecurity: 0, + medicare: 0, + }); + TestBed.configureTestingModule({ + providers: [PayrollService, { provide: TaxService, useValue: taxServiceSpy }], + }); + }); + + it('does some test where it is injected', inject([PayrollService], (service: PayrollService) => { + expect(service).toBeTruthy(); + })); + + it('does some test where it is manually built', () => { + const service = new PayrollService(taxServiceSpy); + expect(service).toBeTruthy(); + }); +}); +``` + +#### Testing HTTP Data Services + +Most services that perform HTTP operations will use Angular's HttpClient service in order to perform those operations. For such tests, it is suggested to use Angular's `HttpClientTestingModule`. For detailed documentation of this module, please refer to Angular's Angular's Testing HTTP requests guide. + +This basic setup for such a test looks like this: + +```tsx +import { HttpBackend, HttpClient } from '@angular/common/http'; +import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed, inject } from '@angular/core/testing'; + +import { IssTrackingDataService } from './iss-tracking-data.service'; + +describe('IssTrackingDataService', () => { + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + let issTrackingDataService: IssTrackingDataService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [IssTrackingDataService], + }); + + httpClient = TestBed.get(HttpClient); + httpTestingController = TestBed.get(HttpTestingController); + issTrackingDataService = new IssTrackingDataService(httpClient); + }); + + it('exists', inject([IssTrackingDataService], (service: IssTrackingDataService) => { + expect(service).toBeTruthy(); + })); + + describe('location', () => { + it('gets the location of the ISS now', () => { + issTrackingDataService.location().subscribe((x) => { + expect(x).toEqual({ longitude: -138.1719, latitude: 44.4423 }); + }); + const req = httpTestingController.expectOne('http://api.open-notify.org/iss-now.json'); + expect(req.request.method).toEqual('GET'); + req.flush({ + iss_position: { longitude: '-138.1719', latitude: '44.4423' }, + timestamp: 1525950644, + message: 'success', + }); + httpTestingController.verify(); + }); + }); +}); +``` + +### Pipes + +A pipe is like a service with a specifically defined interface. It is a class that contains one public method, `transform`, which manipulates the input value (and other optional arguments) in order to create the output that is rendered on the page. To test a pipe: instantiate the pipe, call the transform method, and verify the results. + +As a simple example, let's consider a pipe that takes a `Person` object and formats the name. For the sake of simplicity, let's say a `Person` consists of an `id`, `firstName`, `lastName`, and `middleInitial`. The requirements for the pipe are to print the name as "Last, First M." handling situations where a first name, last name, or middle initial do not exist. Such a test might look like this: + +```tsx +import { NamePipe } from './name.pipe'; + +import { Person } from '../../models/person'; + +describe('NamePipe', () => { + let pipe: NamePipe; + let testPerson: Person; + + beforeEach(() => { + pipe = new NamePipe(); + testPerson = { + id: 42, + firstName: 'Douglas', + lastName: 'Adams', + middleInitial: 'N', + }; + }); + + it('exists', () => { + expect(pipe).toBeTruthy(); + }); + + it('formats a full name properly', () => { + expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas N.'); + }); + + it('handles having no middle initial', () => { + delete testPerson.middleInitial; + expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas'); + }); + + it('handles having no first name', () => { + delete testPerson.firstName; + expect(pipe.transform(testPerson)).toBeEqual('Adams N.'); + }); + + it('handles having no last name', () => { + delete testPerson.lastName; + expect(pipe.transform(testPerson)).toBeEqual('Douglas N.'); + }); +}); +``` + +It is also beneficial to exercise the pipe via DOM testing in the components and pages that utilize the pipe. + +## End-to-end Testing + +End-to-end testing is used to verify that an application works as a whole and often includes a connection to live data. Whereas unit tests focus on code units in isolation and thus allow for low-level testing of the application logic, end-to-end tests focus on various user stories or usage scenarios, providing high-level testing of the overall flow of data through the application. Whereas unit tests try to uncover problems with an application's logic, end-to-end tests try to uncover problems that occur when those individual units are used together. End-to-end tests uncover problems with the overall architecture of the application. + +Since end-to-end tests exercise user stories and cover the application as a whole rather than individual code modules, end-to-end tests exist in their own application in the project apart from the code for the main application itself. Most end-to-end tests operate by automating common user interactions with the application and examining the DOM to determine the results of those interactions. + +### Test Structure + +When an `@ionic/angular` application is generated, a default end-to-end test application is generated in the `e2e` folder. This application uses Protractor to control the browser and Jasmine to structure and execute the tests. The application initially consists of four files: + +- `protractor.conf.js` - the Protractor configuration file +- `tsconfig.e2e.json` - specific TypeScript configuration for the testing application +- `src/app.po.ts` - a page object containing methods that navigate the application, query elements in the DOM, and manipulate elements on the page +- `src/app.e2e-spec.ts` - a testing script + +#### Page Objects + +End-to-end tests operate by automating common user interactions with the application, waiting for the application to respond, and examining the DOM to determine the results of the interaction. This involves a lot of DOM manipulation and examination. If this were all done manually, the tests would be very brittle and difficult to read and maintain. + +Page objects encapsulate the HTML for a single page in a TypeScript class, providing an API that the test scripts use to interact with the application. The encapsulation of the DOM manipulation logic in page objects makes the tests more readable and far easier to reason about, lowering the maintenance costs of the test. Creating well-crafted page objects is the key to creating high quality and maintainable end-to-end tests. + +##### Base Page Object + +A lot of tests rely on actions such as waiting for a page to be visible, entering text into an input, and clicking a button. The methods used to do this remain consistent with only the CSS selectors used to get the appropriate DOM element changing. Therefore it makes sense to abstract this logic into a base class that can be used by the other page objects. + +Here is an example that implements a few basic methods that all page objects will need to support. + +```tsx +import { browser, by, element, ExpectedConditions } from 'protractor'; + +export class PageObjectBase { + private path: string; + protected tag: string; + + constructor(tag: string, path: string) { + this.tag = tag; + this.path = path; + } + + load() { + return browser.get(this.path); + } + + rootElement() { + return element(by.css(this.tag)); + } + + waitUntilInvisible() { + browser.wait(ExpectedConditions.invisibilityOf(this.rootElement()), 3000); + } + + waitUntilPresent() { + browser.wait(ExpectedConditions.presenceOf(this.rootElement()), 3000); + } + + waitUntilNotPresent() { + browser.wait(ExpectedConditions.not(ExpectedConditions.presenceOf(this.rootElement())), 3000); + } + + waitUntilVisible() { + browser.wait(ExpectedConditions.visibilityOf(this.rootElement()), 3000); + } + + getTitle() { + return element(by.css(`${this.tag} ion-title`)).getText(); + } + + protected enterInputText(sel: string, text: string) { + const el = element(by.css(`${this.tag} ${sel}`)); + const inp = el.element(by.css('input')); + inp.sendKeys(text); + } + + protected enterTextareaText(sel: string, text: string) { + const el = element(by.css(`${this.tag} ${sel}`)); + const inp = el.element(by.css('textarea')); + inp.sendKeys(text); + } + + protected clickButton(sel: string) { + const el = element(by.css(`${this.tag} ${sel}`)); + browser.wait(ExpectedConditions.elementToBeClickable(el)); + el.click(); + } +} +``` + +##### Per-Page Abstractions + +Each page in the application will have its own page object class that abstracts the elements on that page. If a base page object class is used, creating the page object involves mostly creating custom methods for elements that are specific to that page. Often, these custom elements take advantage of methods in the base class in order to perform the work that is required. + +Here is an example page object for a simple but typical login page. Notice that many of the methods, such as `enterEMail()`, call methods in the base class that perform the bulk of the work. + +```tsx +import { browser, by, element, ExpectedConditions } from 'protractor'; +import { PageObjectBase } from './base.po'; + +export class LoginPage extends PageObjectBase { + constructor() { + super('app-login', '/login'); + } + + waitForError() { + browser.wait(ExpectedConditions.presenceOf(element(by.css('.error'))), 3000); + } + + getErrorMessage() { + return element(by.css('.error')).getText(); + } + + enterEMail(email: string) { + this.enterInputText('#email-input', email); + } + + enterPassword(password: string) { + this.enterInputText('#password-input', password); + } + + clickSignIn() { + this.clickButton('#signin-button'); + } +} +``` + +#### Testing Scripts + +Similar to unit tests, end-to-end test scripts consist of nested `describe()` and `it()` functions. In the case of end-to-end tests, the `describe()` functions generally denote specific scenarios with the `it()` functions denoting specific behaviors that should be exhibited by the application as actions are performed within that scenario. + +Also similar to unit tests, the labels used in the `describe()` and `it()` functions should make sense both with the "describe" or "it" and when concatenated together to form the complete test case. + +Here is a sample end-to-end test script that exercises some typical login scenarios. + +```tsx +import { AppPage } from '../page-objects/pages/app.po'; +import { AboutPage } from '../page-objects/pages/about.po'; +import { CustomersPage } from '../page-objects/pages/customers.po'; +import { LoginPage } from '../page-objects/pages/login.po'; +import { MenuPage } from '../page-objects/pages/menu.po'; +import { TasksPage } from '../page-objects/pages/tasks.po'; + +describe('Login', () => { + const about = new AboutPage(); + const app = new AppPage(); + const customers = new CustomersPage(); + const login = new LoginPage(); + const menu = new MenuPage(); + const tasks = new TasksPage(); + + beforeEach(() => { + app.load(); + }); + + describe('before logged in', () => { + it('displays the login screen', () => { + expect(login.rootElement().isDisplayed()).toEqual(true); + }); + + it('allows in-app navigation to about', () => { + menu.clickAbout(); + about.waitUntilVisible(); + login.waitUntilInvisible(); + }); + + it('does not allow in-app navigation to tasks', () => { + menu.clickTasks(); + app.waitForPageNavigation(); + expect(login.rootElement().isDisplayed()).toEqual(true); + }); + + it('does not allow in-app navigation to customers', () => { + menu.clickCustomers(); + app.waitForPageNavigation(); + expect(login.rootElement().isDisplayed()).toEqual(true); + }); + + it('displays an error message if the login fails', () => { + login.enterEMail('test@test.com'); + login.enterPassword('bogus'); + login.clickSignIn(); + login.waitForError(); + expect(login.getErrorMessage()).toEqual('The password is invalid or the user does not have a password.'); + }); + + it('navigates to the tasks page if the login succeeds', () => { + login.enterEMail('test@test.com'); + login.enterPassword('testtest'); + login.clickSignIn(); + tasks.waitUntilVisible(); + }); + }); + + describe('once logged in', () => { + beforeEach(() => { + tasks.waitUntilVisible(); + }); + + it('allows navigation to the customers page', () => { + menu.clickCustomers(); + customers.waitUntilVisible(); + tasks.waitUntilInvisible(); + }); + + it('allows navigation to the about page', () => { + menu.clickAbout(); + about.waitUntilVisible(); + tasks.waitUntilInvisible(); + }); + + it('allows navigation back to the tasks page', () => { + menu.clickAbout(); + tasks.waitUntilInvisible(); + menu.clickTasks(); + tasks.waitUntilVisible(); + }); + }); +}); +``` + +### Configuration + +The default configuration uses the same `environment.ts` file that is used for development. In order to provide better control over the data used by the end-to-end tests, it is often useful to create a specific environment for testing and use that environment for the tests. This section shows one possible way to create this configuration. + +#### Testing Environment + +Setting up a testing environment involves creating a new environment file that uses a dedicated testing backend, updating the `angular.json` file to use that environment, and modifying the `e2e` script in the `package.json` to specify the `test` environment. + +##### Create the `environment.e2e.ts` File + +The Angular `environment.ts` and `environment.prod.ts` files are often used to store information such as the base URL for the application's backend data services. Create an `environment.e2e.ts` that provides the same information, only connecting to backend services that are dedicated to testing rather than the development or production backend services. Here is an example: + +```tsx +export const environment = { + production: false, + databaseURL: 'https://e2e-test-api.my-great-app.com', + projectId: 'my-great-app-e2e', +}; +``` + +##### Modify the `angular.json` File + +The `angular.json` file needs to be modified to use this file. This is a layered process. Follow the XPaths listed below to add the configuration that is required. + +Add a configuration at `/projects/app/architect/build/configurations` called `test` that does the file replacement: + +```json +"test": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.e2e.ts" + } + ] +} +``` + +Add a configuration at `/projects/app/architect/serve/configurations` called `test` that points the browser target at the `test` build configuration that was defined above. + +```json +"test": { + "browserTarget": "app:build:test" +} +``` + +Add a configuration at `/projects/app-e2e/architect/e2e/configurations` called `test` that does points the dev server target at the `test` serve configuration defined above. + +```json +"test": { + "devServerTarget": "app:serve:test" +} +``` + +##### Modify the `package.json` File + +Modify the `package.json` file so that `npm run e2e` uses the `test` configuration. + +```json +"scripts": { + "e2e": "ng e2e --configuration=test", + "lint": "ng lint", + "ng": "ng", + "start": "ng serve", + "test": "ng test", + "test:dev": "ng test --browsers=ChromeHeadlessCI", + "test:ci": "ng test --no-watch --browsers=ChromeHeadlessCI" +}, +``` + +#### Test Cleanup + +If the end-to-end tests modify data in any way it is helpful to reset the data to a known state once the test completes. One way to do that is to: + +1. Create an endpoint that performs the cleanup. +1. Add a `onCleanUp()` function to the `config` object exported by the `protractor.conf.js` file. + +Here is an example: + +```javascript +onCleanUp() { + const axios = require('axios'); + return axios + .post( + 'https://e2e-test-api.my-great-app.com/purgeDatabase', + {} + ) + .then(res => { + console.log(res.data); + }) + .catch(err => console.log(err)); +} +``` diff --git a/docs/angular/virtual-scroll.md b/docs/angular/virtual-scroll.md new file mode 100644 index 00000000000..0ee3289bf3d --- /dev/null +++ b/docs/angular/virtual-scroll.md @@ -0,0 +1,133 @@ +# Virtual Scroll + +:::warning[Looking for `ion-virtual-scroll`?] + +`ion-virtual-scroll` was deprecated in v6.0.0 and removed in v7.0.0. We recommend using the `@angular/cdk` package detailed below. + +::: + +## Installation + +To setup the CDK Scroller, first install `@angular/cdk`: + +```shell +npm add @angular/cdk +``` + +This provides a collection of different utilities, but we'll focus on `ScrollingModule` for now. + +When we want to use the CDK Scroller, we'll need to import the module in our component. For example, in a tabs starter project, we can add our import to the `tabs1.module.ts` file. + +```diff + import { IonicModule } from '@ionic/angular/lazy'; + import { NgModule } from '@angular/core'; + import { CommonModule } from '@angular/common'; + import { FormsModule } from '@angular/forms'; + import { Tab1Page } from './tab1.page'; + import { ExploreContainerComponentModule } from '../explore-container/explore-container.module'; ++ import { ScrollingModule } from '@angular/cdk/scrolling'; + import { Tab1PageRoutingModule } from './tab1-routing.module'; + @NgModule({ + imports: [ + IonicModule, + CommonModule, + FormsModule, + ExploreContainerComponentModule, + Tab1PageRoutingModule, ++ ScrollingModule + ], + declarations: [Tab1Page] + }) + export class Tab1PageModule {} +``` + +With this added, we have access to the Virtual Scroller in the Tab1Page component. + +## Usage + +The CDK Virtual Scroller can be added to a component by adding the `cdk-virtual-scroll-viewport` to a component's template. + +```html + + + +``` + +`cdk-virtual-scroll-viewport` becomes the root of our scrollable content and is responsible for recycling DOM nodes as they scroll out of view. + +The DOM nodes at this point can be any content needed for an app. The difference is that when we want to iterate over a collection, `*cdkVirtualFor` is used instead of `*ngFor`. + +```html + + + + + + + + {{item }} + + + + +``` + +Here, `items` is an array, but it can be an array, `Observable`, or `DataSource`. `DataSource` is an abstract class that can provide the data needed as well as utility methods. For more details, check out the [CDK Virtual Scrolling docs](https://material.angular.io/cdk/scrolling/overview). + +The component is not complete yet as the `cdk-virtual-scroll-viewport` needs to know how big each node will be as well as the min/max buffer sizes. + +At the moment, CDK Virtual Scroller only supports fixed sized elements, but dynamic sized elements are planned for the future. For the `Tab1Page` component, since it is only rendering an item, it can be hard-coded to a fixed size. + +The min/max buffer size tells the scroller "render as many nodes as it takes to meet this minimum height, but not over this". + +```html + +``` + +For this case, the `cdk-virtual-scroll-viewport` will render cells at a height 56px until it reaches a height of 900px, but no more at 1350px. These numbers are arbitrary, so be sure to test out what values will work in a real use case. + +Putting everything together, the final HTML should look like: + +```html + + + + + + + + {{item }} + + + + +``` + +The last piece needed is a some CSS to size the viewport correctly. In the `tab1.page.scss` file, add the following + +```scss +cdk-virtual-scroll-viewport { + height: 100%; + width: 100%; +} +``` + +Since the viewport is built to fit various use cases, the default sizing is not set and is up to developers to set. + +## Usage with Ionic Components + +Ionic Framework requires that features such as collapsible large titles, `ion-infinite-scroll`, `ion-refresher`, and `ion-reorder-group` be used within an `ion-content`. To use these experiences with virtual scrolling, you must add the `.ion-content-scroll-host` class to the virtual scroll viewport. + +For example: + +```html + + + + + +``` + +## Further Reading + +This only covers a small portion of what the CDK Virtual Scroller is capable of. For more details, please refer to the [Angular CDK Virtual Scrolling docs](https://material.angular.io/cdk/scrolling/overview). diff --git a/docs/angular/your-first-app.md b/docs/angular/your-first-app.md new file mode 100644 index 00000000000..e21a9458f36 --- /dev/null +++ b/docs/angular/your-first-app.md @@ -0,0 +1,246 @@ +--- +title: 'Your First Ionic App: Angular' +sidebar_label: Build Your First App +--- + + + Build Your First Ionic Mobile App with Angular | Ionic Capacitor Camera + + + +The great thing about Ionic is that with one codebase, you can build for any platform using just HTML, CSS, and JavaScript. Follow along as we learn the fundamentals of Ionic app development by creating a realistic app step by step. + +Here’s the finished app running on all 3 platforms: + + + +:::note +Looking for the previous version of this guide that covered Ionic 4 and Cordova? Refer to the [Ionic 4 and Cordova guide](../developer-resources/guides/first-app-v4/intro.md). +::: + +## What We'll Build + +We'll create a Photo Gallery app that offers the ability to take photos with your device's camera, display them in a grid, and store them permanently on the device. + +Highlights include: + +- One Angular-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](../components.md). +- Deployed as a native iOS and Android mobile app using [Capacitor](https://capacitorjs.com), Ionic's official native app runtime. +- Photo Gallery functionality powered by the Capacitor [Camera](../native/camera.md), [Filesystem](../native/filesystem.md), and [Preferences](../native/preferences.md) APIs. + +Find the [complete app code](https://github.com/ionic-team/tutorial-photo-gallery-angular) referenced in this guide on GitHub. + +## Download Required Tools + +Download and install these right away to ensure an optimal Ionic development experience: + +- **Node.js** for interacting with the Ionic ecosystem. [Download the LTS version](https://nodejs.org/en/). +- **A code editor** for... writing code! We are fans of [Visual Studio Code](https://code.visualstudio.com/). +- **Command-line interface/terminal (CLI)**: + - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell CLI, running in Administrator mode. + - **Mac/Linux** users: virtually any terminal will work. + +## Install Ionic Tooling + +Run the following in the command line terminal to install the Ionic CLI (`ionic`), `native-run`, used to run native binaries on devices and simulators/emulators, and `cordova-res`, used to generate native app icons and splash screens: + +:::note +To open a terminal in Visual Studio Code, go to Terminal -> New Terminal. +::: + +```shell +npm install -g @ionic/cli native-run cordova-res +``` + +:::note +The `-g` option means _install globally_. When packages are installed globally, `EACCES` permission errors can occur. + +Consider setting up npm to operate globally without elevated permissions. Refer to [Resolving Permission Errors](../developing/tips.md#resolving-permission-errors) for more information. +::: + +## Create an App + +Next, create an Ionic Angular app that uses the "Tabs" starter template and adds Capacitor for native functionality: + +```shell +ionic start photo-gallery tabs --type=angular +``` + +:::note + +When prompted to choose between `NgModules` and `Standalone`, choose `Standalone` as this tutorial follows the standalone components approach. + +::: + +This starter project comes complete with three pre-built pages and best practices for Ionic development. With common building blocks already in place, we can add more features easily! + +Next, change into the app folder: + +```shell +cd photo-gallery +``` + +Next we'll need to install the necessary Capacitor plugins to make the app's native functionality work: + +```shell +npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem +``` + +### PWA Elements + +Some Capacitor plugins, including the [Camera API](../native/camera.md), provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). + +It's a separate dependency, so install it next: + +```shell +npm install @ionic/pwa-elements +``` + +Next, import `@ionic/pwa-elements` by editing `src/main.ts`. + +```ts +import { bootstrapApplication } from '@angular/platform-browser'; +import { RouteReuseStrategy, provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; +import { IonicRouteStrategy, provideIonicAngular } from '@ionic/angular'; +// CHANGE: Add the following import +import { defineCustomElements } from '@ionic/pwa-elements/loader'; + +import { routes } from './app/app.routes'; +import { AppComponent } from './app/app.component'; + +// CHANGE: Call the element loader before the `bootstrapApplication` call +defineCustomElements(window); + +bootstrapApplication(AppComponent, { + providers: [ + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, + provideIonicAngular(), + provideRouter(routes, withPreloading(PreloadAllModules)), + ], +}).catch((err) => console.error(err)); +``` + +That’s it! Now for the fun part - let’s run the app. + +## Run the App + +Run this command next: + +```shell +ionic serve +``` + +And voilà! Your Ionic app is now running in a web browser. Most of your app can be built and tested right in the browser, greatly increasing development and testing speed. + +## Photo Gallery + +There are three tabs. Click on the "Tab2" tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! + +![Animated GIF showing the live reload feature in an Ionic app, with changes in code immediately updating the app in a web browser.](/img/guides/first-app-cap-ng/email-photogallery.gif 'Live Reload Feature in Ionic App') + +Open `/src/app/tab2/tab2.page.html`. It contains: + +```html + + + Tab 2 + + + + + + + Tab 2 + + + + + +``` + +`ion-header` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title.md#collapsible-large-titles) support). Rename both `ion-title` elements to: + +```html + + + + Photo Gallery + + + + + + + + Photo Gallery + + + + + +``` + +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab.md) (FAB) to the bottom of the page and set the camera image as the icon. + +```html + + + Photo Gallery + + + + + + + Photo Gallery + + + + + + + + + + + + + +``` + +Next, open `src/app/tabs/tabs.page.html`. Change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button. + +```html + + + + + Tab 1 + + + + + + + Photos + + + + + Tab 3 + + + +``` + +That’s just the start of all the cool things we can do with Ionic. Up next, implement camera taking functionality on the web, then build it for iOS and Android. diff --git a/docs/angular/your-first-app/2-taking-photos.md b/docs/angular/your-first-app/2-taking-photos.md new file mode 100644 index 00000000000..92ffeeb2b36 --- /dev/null +++ b/docs/angular/your-first-app/2-taking-photos.md @@ -0,0 +1,270 @@ +--- +title: Taking Photos with the Camera +sidebar_label: Taking Photos +--- + + + Take Photos with Camera API for iOS, Android & Web with Angular | Ionic Capacitor Camera + + + +Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](../../native/camera.md). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). + +## Photo Service + +All Capacitor logic (Camera usage and other native features) will be encapsulated in a service class. Create `PhotoService` using the `ionic generate` command: + +```shell +ionic g service services/photo.service +``` + +Open the new `services/photo.service.ts` file, and let’s add the logic that will power the camera functionality. First, import Capacitor dependencies and get references to the `Camera`, `Filesystem`, and `Storage` plugins: + +```ts +import { Injectable } from '@angular/core'; +// CHANGE: Add the following import +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService {} +``` + +Next, define a new class method, `addNewToGallery()`, that will contain the core logic to take a device photo and save it to the filesystem. Let’s start by opening the device camera. + +```ts +import { Injectable } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + // CHANGE: Add the gallery method + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + } +} +``` + +Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `Camera.getPhoto()` - that will open up the device's camera and allow us to take photos. + +Next, in `tab2.page.ts`, import the `PhotoService` class and add a method to call its `addNewToGallery` method. + +```ts +import { Component, inject } from '@angular/core'; +// CHANGE: Import the Ionic standalone components used on this page +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonFab, + IonFabButton, + IonIcon, +} from '@ionic/angular'; +// CHANGE: Register the camera icon used by the FAB +import { addIcons } from 'ionicons'; +import { camera } from 'ionicons/icons'; +// CHANGE: Import the PhotoService +import { PhotoService } from '../services/photo.service'; + +@Component({ + selector: 'app-tab2', + templateUrl: 'tab2.page.html', + styleUrls: ['tab2.page.scss'], + // CHANGE: Add the standalone component imports + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonGrid, IonRow, IonCol, IonFab, IonFabButton, IonIcon], +}) +export class Tab2Page { + // CHANGE: Inject the PhotoService + public photoService = inject(PhotoService); + + constructor() { + // CHANGE: Register the icons this page uses + addIcons({ camera }); + } + + // CHANGE: Add `addPhotoToGallery()` method + addPhotoToGallery() { + this.photoService.addNewToGallery(); + } +} +``` + +:::note +In a standalone app there is no global icon registry, so each icon you reference by name (like `camera`) must be registered with `addIcons`. Import the specific Ionic components a page uses from `@ionic/angular` and list them in the component's `imports` array. +::: + +Then, open `tab2.page.html` and call the `addPhotoToGallery()` method when the FAB is tapped/clicked: + +```html + + + Photo Gallery + + + + + + + Photo Gallery + + + + + + + + + + +``` + +If it's not running already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! + +![A photo gallery app displaying a webcam selfie.](/img/guides/first-app-cap-ng/camera-web.png 'Webcam Selfie in Photo Gallery') + +_(Your selfie is probably much better than mine)_ + +After taking a photo, it disappears right away. We need to display it within our app and save it for future access. + +## Displaying Photos + +To define the data structure for our photo metadata, create a new interface named `UserPhoto`. Add this interface at the very bottom of the `photo.service.ts` file, immediately after the `PhotoService` class definition: + +```ts +export class PhotoService { + // ...existing code... +} + +// CHANGE: Add the `UserPhoto` interface +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Above the `addNewToGallery()` method, define a [signal](https://angular.dev/guide/signals) that holds an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. A signal is used so that the gallery view updates automatically when photos change - important in a zoneless app, where mutating a plain array would not trigger a re-render. + +```ts +export class PhotoService { + // CHANGE: Add the `photos` signal + public photos = signal([]); + + public async addNewToGallery() { + // ...existing code... + } +} +``` + +Over in the `addNewToGallery` method, add the newly captured photo to the beginning of the `photos` signal. Reading and updating a signal is done by calling it: `this.photos()` returns the current value, and `this.photos.update()` sets a new one. + +```ts +// CHANGE: Update `addNewToGallery()` method +public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100 + }); + + // CHANGE: Add the new photo to the front of the photos signal + this.photos.update((photos) => [ + { + filepath: 'soon...', + webviewPath: capturedPhoto.webPath!, + }, + ...photos, + ]); +} +``` + +`photo.service.ts` should now look like this: + +```ts +import { Injectable, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + this.photos.update((photos) => [ + { + filepath: 'soon...', + webviewPath: capturedPhoto.webPath!, + }, + ...photos, + ]); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next, switch to `tab2.page.html` to display the images. We'll add a [Grid component](../../api/grid.md) so the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `PhotoService`'s `photos` signal with the built-in [`@for`](https://angular.dev/guide/templates/control-flow#for-block-repeaters) block - calling `photoService.photos()` reads the signal's current value. For each item, add an `` element and set its `src` property to the photo's path. + +```html + + + Photo Gallery + + + + + + + Photo Gallery + + + + + + + + @for (photo of photoService.photos(); track photo.filepath; let position = $index) { + + + + } + + + + + + + + + +``` + +Within the web browser, click the camera button and take another photo. This time, the photo is displayed in the Photo Gallery! + +Up next, we’ll add support for saving the photos to the filesystem, so they can be retrieved and displayed in our app at a later time. diff --git a/docs/angular/your-first-app/3-saving-photos.md b/docs/angular/your-first-app/3-saving-photos.md new file mode 100644 index 00000000000..2986081b7f4 --- /dev/null +++ b/docs/angular/your-first-app/3-saving-photos.md @@ -0,0 +1,220 @@ +--- +title: Saving Photos to the Filesystem +sidebar_label: Saving Photos +--- + + + Saving Photos to the Filesystem with Angular | Ionic Capacitor Camera + + + +We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be deleted. + +## Filesystem API + +Fortunately, saving them to the filesystem only takes a few steps. Begin by creating a new class method, `savePicture()`, in the `PhotoService` class. We pass in the `photo` object, which represents the newly captured device photo: + +```ts +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +// CHANGE: Add import +import type { Photo } from '@capacitor/camera'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + // ...existing code... + + // CHANGE: Add the `savePicture()` method + private async savePicture(photo: Photo) { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We can use this new method immediately in `addNewToGallery()`. + +```ts +import { Injectable, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + // CHANGE: Update the `addNewToGallery()` method + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + // CHANGE: Add `savedImageFile` + // Save the picture and add it to photo collection + const savedImageFile = await this.savePicture(capturedPhoto); + + // CHANGE: Add the saved photo to the front of the photos signal + this.photos.update((photos) => [savedImageFile, ...photos]); + } + + private async savePicture(photo: Photo) { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We'll use the Capacitor [Filesystem API](../../native/filesystem.md) to save the photo. First, convert the photo to base64 format. + +Then, pass the data to the Filesystem's `writeFile` method. Recall that we display photos by setting the image's source path (`src`) to the `webviewPath` property. So, set the `webviewPath` and return the new `Photo` object. + +For now, create a new helper method, `convertBlobToBase64()`, to implement the necessary logic for running on the web. + +```ts +import { Injectable } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +// CHANGE: Add import +import { Filesystem, Directory } from '@capacitor/filesystem'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + // ...existing code... + + // CHANGE: Update the `savePicture()` method + private async savePicture(photo: Photo) { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await this.convertBlobToBase64(blob)) as string; + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + + // CHANGE: Add the `convertBlobToBase64` method + private convertBlobToBase64(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +`photo.service.ts` should now look like this: + +```ts +import { Injectable, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + // Save the picture and add it to photo collection + const savedImageFile = await this.savePicture(capturedPhoto); + + this.photos.update((photos) => [savedImageFile, ...photos]); + } + + private async savePicture(photo: Photo) { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await this.convertBlobToBase64(blob)) as string; + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + + private convertBlobToBase64(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Obtaining the camera photo as base64 format on the web appears to be a bit trickier than on mobile. In reality, we’re just using built-in web APIs: [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as a neat way to read the file into blob format, then FileReader’s [readAsDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) to convert the photo blob to base64. + +There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. Next up, we'll load and display our saved images. diff --git a/docs/angular/your-first-app/4-loading-photos.md b/docs/angular/your-first-app/4-loading-photos.md new file mode 100644 index 00000000000..bf14ebd7380 --- /dev/null +++ b/docs/angular/your-first-app/4-loading-photos.md @@ -0,0 +1,270 @@ +--- +title: Loading Photos from the Filesystem +sidebar_label: Loading Photos +--- + + + Loading Photos from the Filesystem with Angular | Ionic Capacitor Camera + + + +We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery. + +Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](../../native/preferences.md) to store our array of Photos in a key-value store. + +## Preferences API + +Open `photo.service.ts` and begin by defining a new property in the `PhotoService` class that will act as the key for the store. + +```ts +export class PhotoService { + public photos = signal([]); + + // CHANGE: Add a key for photo storage + private PHOTO_STORAGE: string = 'photos'; + + // ...existing code... +} +``` + +Next, at the end of the `addNewToGallery()` method, add a call to `Preferences.set()` to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. + +```ts +import { Injectable } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +// CHANGE: Add import +import { Preferences } from '@capacitor/preferences'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + // ...existing code... + + // CHANGE: Update `addNewToGallery()` method + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const savedImageFile = await this.savePicture(capturedPhoto); + + this.photos.update((photos) => [savedImageFile, ...photos]); + + // CHANGE: Add method to cache all photo data for future retrieval + Preferences.set({ + key: this.PHOTO_STORAGE, + value: JSON.stringify(this.photos()), + }); + } + + // ...existing code... +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +With the photo array data saved, create a new public method in the `PhotoService` class called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array: + +```ts +export class PhotoService { + // ...existing code... + + // CHANGE: Add the method to load the photo data + public async loadSaved() { + // Retrieve cached photo array data + const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); + this.photos.set((photoList ? JSON.parse(photoList) : []) as UserPhoto[]); + } +} +``` + +On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method: + +```ts +export class PhotoService { + // ...existing code... + + // CHANGE: Update the `loadSaved()` method + public async loadSaved() { + // Retrieve cached photo array data + const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // CHANGE: Display the photo by reading into base64 format + for (const photo of photos) { + // Read each saved photo's data from the Filesystem + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + // CHANGE: Set the signal so the gallery view updates + this.photos.set(photos); + } +} +``` + +`photo.service.ts` should now look like this: + +```ts +import { Injectable, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + private PHOTO_STORAGE: string = 'photos'; + + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + // Save the picture and add it to photo collection + const savedImageFile = await this.savePicture(capturedPhoto); + + this.photos.update((photos) => [savedImageFile, ...photos]); + + Preferences.set({ + key: this.PHOTO_STORAGE, + value: JSON.stringify(this.photos()), + }); + } + + private async savePicture(photo: Photo) { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await this.convertBlobToBase64(blob)) as string; + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + + private convertBlobToBase64(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + } + + public async loadSaved() { + // Retrieve cached photo array data + const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + for (const photo of photos) { + // Read each saved photo's data from the Filesystem + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + this.photos.set(photos); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Our `PhotoService` can now load the saved images, but we'll need to update `tab2.page.ts` to put that new code to work. We'll call `loadSaved()` within the [ngOnInit](https://angular.dev/guide/components/lifecycle#ngoninit) lifecycle method so that when the user first navigates to the Photo Gallery, all photos are loaded and displayed on the screen. + +Update `tab2.page.ts` to look like the following: + +```ts +import { Component, OnInit, inject } from '@angular/core'; +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonFab, + IonFabButton, + IonIcon, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +import { camera } from 'ionicons/icons'; +import { PhotoService } from '../services/photo.service'; + +@Component({ + selector: 'app-tab2', + templateUrl: 'tab2.page.html', + styleUrls: ['tab2.page.scss'], + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonGrid, IonRow, IonCol, IonFab, IonFabButton, IonIcon], +}) +export class Tab2Page implements OnInit { + public photoService = inject(PhotoService); + + constructor() { + addIcons({ camera }); + } + + // CHANGE: Add call to `loadSaved()` when navigating to the Photos tab + async ngOnInit() { + await this.photoService.loadSaved(); + } + + addPhotoToGallery() { + this.photoService.addNewToGallery(); + } +} +``` + +:::note +If you encounter broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb). + +In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`. +::: + +That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! diff --git a/docs/angular/your-first-app/5-adding-mobile.md b/docs/angular/your-first-app/5-adding-mobile.md new file mode 100644 index 00000000000..21d10528ae1 --- /dev/null +++ b/docs/angular/your-first-app/5-adding-mobile.md @@ -0,0 +1,300 @@ +--- +title: Adding Mobile +strip_number_prefixes: false +--- + + + Adding Mobile Support with Angular | Ionic Capacitor Camera + + + +Our photo gallery app won’t be complete until it runs on iOS, Android, and the web - all using one codebase. All it takes is some small logic changes to support mobile platforms, installing some native tooling, then running the app on a device. Let’s go! + +## Import Platform API + +Let’s start with making some small code changes - then our app will “just work” when we deploy it to a device. + +Import the Ionic [Platform API](../platform.md) into `photo.service.ts`, which is used to retrieve information about the current device. In this case, it’s useful for selecting which code to execute based on the platform the app is running on (web or mobile). + +Add `Platform` to the imports at the top of the file and a new property `platform` to the `PhotoService` class. We'll also need to update the constructor to set the user's platform. + +```ts +import { Injectable, inject, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +// CHANGE: Add import +import { Platform } from '@ionic/angular'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + private PHOTO_STORAGE: string = 'photos'; + + // CHANGE: Inject the Platform API to track the app's running platform + private platform = inject(Platform); + + // ...existing code... +} +``` + +## Platform-specific Logic + +First, we’ll update the photo saving functionality to support mobile. In the `savePicture()` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem.readFile()` method. Otherwise, use the same logic as before when running the app on the web. + +Update `savePicture()` to look like the following: + +```ts +// CHANGE: Update the `savePicture()` method +private async savePicture(photo: Photo) { + let base64Data: string | Blob; + + // CHANGE: Add platform check + // "hybrid" will detect Cordova or Capacitor + if (this.platform.is('hybrid')) { + // Read the file into base64 format + const file = await Filesystem.readFile({ + path: photo.path! + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = await this.convertBlobToBase64(blob) as string; + } + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; +} +``` + +When running on mobile, set `filepath` to the result of the `writeFile()` operation - `savedFile.uri`. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc()` method ([details on the File Protocol](../../core-concepts/webview.md#file-protocol)). To use this method, we'll need to import Capacitor into `photo.service.ts`. + +```ts +import { Injectable, inject, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Platform } from '@ionic/angular'; +// Change: Add import +import { Capacitor } from '@capacitor/core'; + +// ...existing code... +``` + +Then update `savePicture()` to look like the following: + +```ts +// CHANGE: Update `savePicture()` method +private async savePicture(photo: Photo) { + let base64Data: string | Blob; + // "hybrid" will detect mobile - iOS or Android + if (this.platform.is('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = await this.convertBlobToBase64(blob) as string; + } + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // CHANGE: Add platform check + if (this.platform.is('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } +} +``` + +Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved()` method: + +```ts +// CHANGE: Update `loadSaved()` method +public async loadSaved() { + const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // CHANGE: Add platform check + // If running on the web... + if (!this.platform.is('hybrid')) { + for (const photo of photos) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data + }); + + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + // CHANGE: Set the signal so the gallery view updates + this.photos.set(photos); +} +``` + +Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. + +`photos.service.ts` should now look like this: + +```ts +import { Injectable, inject, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Platform } from '@ionic/angular'; +import { Capacitor } from '@capacitor/core'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + public photos = signal([]); + + private PHOTO_STORAGE: string = 'photos'; + + private platform = inject(Platform); + + public async addNewToGallery() { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const savedImageFile = await this.savePicture(capturedPhoto); + + this.photos.update((photos) => [savedImageFile, ...photos]); + + Preferences.set({ + key: this.PHOTO_STORAGE, + value: JSON.stringify(this.photos()), + }); + } + + private async savePicture(photo: Photo) { + let base64Data: string | Blob; + + // "hybrid" will detect Cordova or Capacitor + if (this.platform.is('hybrid')) { + // Read the file into base64 format + const file = await Filesystem.readFile({ + path: photo.path!, + }); + + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + + base64Data = (await this.convertBlobToBase64(blob)) as string; + } + + // Write the file to the data directory + const fileName = Date.now() + '.jpeg'; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (this.platform.is('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + } + + private convertBlobToBase64(blob: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + } + + public async loadSaved() { + // Retrieve cached photo array data + const { value: photoList } = await Preferences.get({ key: this.PHOTO_STORAGE }); + const photos = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // If running on the web... + if (!this.platform.is('hybrid')) { + for (const photo of photos) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + this.photos.set(photos); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next up, the part you’ve been waiting for - deploying the app to a device. diff --git a/docs/angular/your-first-app/6-deploying-mobile.md b/docs/angular/your-first-app/6-deploying-mobile.md new file mode 100644 index 00000000000..11bf04154b0 --- /dev/null +++ b/docs/angular/your-first-app/6-deploying-mobile.md @@ -0,0 +1,114 @@ +--- +title: Deploying to iOS and Android +sidebar_label: Deploying Mobile +--- + + + Adding Mobile Support with Angular | Ionic Capacitor Camera + + + +Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! + +## Capacitor Setup + +Capacitor is Ionic’s official app runtime that makes it easy to deploy web apps to native platforms like iOS, Android, and more. If you’ve used Cordova in the past, consider reading more about the [differences between Capacitor and Cordova](https://capacitorjs.com/docs/cordova#differences-between-capacitor-and-cordova). + +If you’re still running `ionic serve` in the terminal, cancel it. Complete a fresh build of the Ionic project, fixing any errors that it reports: + +```shell +ionic build +``` + +Next, create both the iOS and Android projects: + +```shell +ionic cap add ios +ionic cap add android +``` + +Both android and ios folders at the root of the project are created. These are entirely standalone native projects that should be considered part of your Ionic app (i.e., check them into source control, edit them using their native tooling, etc.). + +Every time you perform a build (e.g. `ionic build`) that updates your web directory (default: `www`), you'll need to copy those changes into your native projects: + +```shell +ionic cap copy +``` + +Note: After making updates to the native portion of the code (such as adding a new plugin), use the `sync` command: + +```shell +ionic cap sync +``` + +## iOS Deployment + +:::important +To build an iOS app, you’ll need a Mac computer. +::: + +Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by [CocoaPods](https://cocoapods.org/). Before running this app on an iOS device, there's a couple of steps to complete. + +First, run the Capacitor `open` command, which opens the native iOS project in Xcode: + +```shell +ionic cap open ios +``` + +In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled "Privacy - Camera Usage." To set it, the `Info.plist` file must be modified ([more details on iOS configuration](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." + +![The Info.plist file in Xcode showing the NSCameraUsageDescription key added for camera access.](/img/guides/first-app-cap-ng/xcode-info-plist.png 'Xcode Info.plist Configuration') + +Each setting in `Info.plist` has a low-level parameter name and a high-level name. By default, the property list editor shows the high-level names, but it's often useful to switch to showing the raw, low-level names. To do this, right-click anywhere in the property list editor and toggle "Raw Keys/Values." + +Add the `NSCameraUsageDescription` Key and set the Value to something that describes why the app needs to use the camera, such as "To Take Photos." The Value field is displayed to the app user when the permission prompt opens. + +Follow the same process to add the other two Keys required of the Camera plugin: `NSPhotoLibraryAddUsageDescription` and `NSPhotoLibraryUsageDescription`. + +Next, click on `App` in the Project Navigator on the left-hand side, then within the `Signing & Capabilities` section, select your Development Team. + +![The Xcode interface displaying the Signing and Capabilities tab for an iOS app project.](/img/guides/first-app-cap-ng/xcode-signing.png 'Xcode Signing & Capabilities') + +With permissions in place and Development Team selected, we are ready to try out the app on a real device! Connect an iOS device to your Mac computer, select it (`App -> Matthew’s iPhone` for me) then click the "Build" button to build, install, and launch the app on your device: + +![Xcode toolbar highlighting the Build button used to compile and run an iOS app.](/img/guides/first-app-cap-ng/xcode-build-button.png 'Xcode Build Button') + +Upon tapping the Camera button on the Photo Gallery tab, the permission prompt will display. Tap OK, then take a picture with the Camera. Afterward, the photo shows in the app! + +![Two iPhones side by side, one showing the camera permission prompt and the other displaying a photo taken with the app.](/img/guides/first-app-cap-ng/ios-permissions-photo.png 'iOS Camera Permission Prompt and Photo Result') + +## Android Deployment + +Capacitor Android apps are configured and managed through Android Studio. Before running this app on an Android device, there's a couple of steps to complete. + +First, run the Capacitor `open` command, which opens the native Android project in Android Studio: + +```shell +ionic cap open android +``` + +Similar to iOS, we must enable the correct permissions to use the Camera. Configure these in the `AndroidManifest.xml` file. Android Studio will likely open this file automatically, but in case it doesn't, locate it under `android/app/src/main/`. + +![Android Studio editor showing the AndroidManifest.xml file with camera permissions.](/img/guides/first-app-cap-ng/android-manifest.png 'Android Manifest Permissions') + +Scroll to the `Permissions` section and ensure these entries are included: + +```xml + + +``` + +Save the file. With permissions in place, we are ready to try out the app on a real device! Connect an Android device to your computer. Within Android Studio, click the "Run" button, select the attached Android device, then click OK to build, install, and launch the app on your device. + +![The Android Studio interface with arrows pointing to the Run button and the connected device.](/img/guides/first-app-cap-ng/android-device.png 'Android Studio Run Configuration') + +Once again, upon tapping the Camera button on the Photo Gallery tab, the permission prompt should be displayed. Tap OK, then take a picture with the Camera. Afterward, the photo should appear in the app. + +![Two Android phones side by side, one showing the camera permission prompt and the other displaying a photo taken with the app.](/img/guides/first-app-cap-ng/android-permissions-photo.png 'Android Permissions and Photo Capture') + +Our Photo Gallery app has just been deployed to Android and iOS devices. 🎉 + +In the final portion of this tutorial, we’ll use the Ionic CLI’s Live Reload functionality to quickly implement photo deletion - thus completing our Photo Gallery feature. diff --git a/docs/angular/your-first-app/7-live-reload.md b/docs/angular/your-first-app/7-live-reload.md new file mode 100644 index 00000000000..7a697854dcc --- /dev/null +++ b/docs/angular/your-first-app/7-live-reload.md @@ -0,0 +1,223 @@ +--- +title: Rapid App Development with Live Reload +sidebar_label: Live Reload +--- + + + Rapid App Development with Live Reload with Angular | Ionic Capacitor Camera + + + +So far, we’ve learned how easy it is to develop a cross-platform app that works everywhere. The development experience is pretty quick, but what if I told you there was a way to go faster? + +We can use the Ionic CLI’s [Live Reload functionality](../../cli/livereload.md) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. + +## Live Reload + +Remember `ionic serve`? That was Live Reload working in the browser, allowing us to iterate quickly. + +We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins - we must run it on a device to verify that it works. Therefore, being able to quickly write, build, test, and deploy code is crucial to keeping up our development speed. + +Let’s use Live Reload to implement photo deletion, the missing piece of our Photo Gallery feature. Select your platform of choice (iOS or Android) and connect a device to your computer. Next, run either command in a terminal, based on your chosen platform: + +```shell +ionic cap run ios -l --external + +ionic cap run android -l --external +``` + +The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. + +## Deleting Photos + +With Live Reload running and the app open on your device, let’s implement photo deletion functionality. + +In `photo.service.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we use the Capacitor Preferences API to update the cached version of the `photos` array. Finally, we delete the actual photo file itself using the Filesystem API. + +```ts +import { Injectable, inject, signal } from '@angular/core'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import type { Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Platform } from '@ionic/angular'; +import { Capacitor } from '@capacitor/core'; + +@Injectable({ + providedIn: 'root', +}) +export class PhotoService { + // ...existing code... + + // CHANGE: Add `deletePhoto()` method + public async deletePhoto(photo: UserPhoto, position: number) { + // Remove this photo from the photos signal + this.photos.update((photos) => photos.filter((_, index) => index !== position)); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ + key: this.PHOTO_STORAGE, + value: JSON.stringify(this.photos()), + }); + + // Delete photo file from filesystem + const filename = photo.filepath.slice(photo.filepath.lastIndexOf('/') + 1); + + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + } +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next, in `tab2.page.ts`, implement the `showActionSheet()` method. We're adding two options: "Delete", which calls `PhotoService.deletePhoto()`, and "Cancel". The cancel button will automatically close the action sheet when assigned the "cancel" role. + +```ts +import { Component, OnInit, inject } from '@angular/core'; +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonGrid, + IonRow, + IonCol, + IonFab, + IonFabButton, + IonIcon, + // CHANGE: Add import + ActionSheetController, +} from '@ionic/angular'; +import { addIcons } from 'ionicons'; +// CHANGE: Register the `trash` and `close` icons used by the action sheet +import { camera, trash, close } from 'ionicons/icons'; +// Change: Add import +import type { UserPhoto } from '../services/photo.service'; +import { PhotoService } from '../services/photo.service'; + +@Component({ + selector: 'app-tab2', + templateUrl: 'tab2.page.html', + styleUrls: ['tab2.page.scss'], + imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonGrid, IonRow, IonCol, IonFab, IonFabButton, IonIcon], +}) +export class Tab2Page implements OnInit { + public photoService = inject(PhotoService); + // CHANGE: Inject the ActionSheetController + private actionSheetController = inject(ActionSheetController); + + constructor() { + // CHANGE: Register the icons this page uses + addIcons({ camera, trash, close }); + } + + async ngOnInit() { + await this.photoService.loadSaved(); + } + + addPhotoToGallery() { + this.photoService.addNewToGallery(); + } + + // CHANGE: Add `showActionSheet()` method + public async showActionSheet(photo: UserPhoto, position: number) { + const actionSheet = await this.actionSheetController.create({ + header: 'Photos', + buttons: [ + { + text: 'Delete', + role: 'destructive', + icon: 'trash', + handler: () => { + this.photoService.deletePhoto(photo, position); + }, + }, + { + text: 'Cancel', + icon: 'close', + role: 'cancel', + handler: () => { + // Nothing to do, action sheet is automatically closed + }, + }, + ], + }); + await actionSheet.present(); + } +} +``` + +Open `tab2.page.html` and wrap each image in a ` + + } + + + + + + + + + +``` + +Add the following CSS to `tab2.page.scss` to style the gallery buttons and images: + +```css +ion-col > button { + display: block; + width: 100%; + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; + font: inherit; +} + +button img { + display: block; + width: 100%; + height: auto; +} +``` + +Tap on a photo again and choose the “Delete” option. The photo is deleted! Implemented much faster using Live Reload. 💪 + +:::note +Remember, you can find the [complete source code for this app](https://github.com/ionic-team/tutorial-photo-gallery-angular) on GitHub. +::: + +In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. diff --git a/docs/angular/your-first-app/8-distribute.md b/docs/angular/your-first-app/8-distribute.md new file mode 100644 index 00000000000..6c221cc4b42 --- /dev/null +++ b/docs/angular/your-first-app/8-distribute.md @@ -0,0 +1,107 @@ +--- +title: Build and Distribute your App +sidebar_label: Distribute +--- + + + Build and Deploy your App with Angular | Ionic Capacitor Camera + + + +Now that you have built your first app, you are going to want to get it distributed so everyone can start using it. The mechanics of building and deploying your application can be quite cumbersome. That is where [Appflow](https://ionic.io/docs/appflow/) comes into play. Appflow allows you to effectively generate web and native builds, push out live app updates, publish your app to the app stores, and automate the whole process. Refer to the [Appflow Quickstart guide](https://ionic.io/docs/appflow/quickstart). + +Below we will run through an overview of the steps. + +## Connect Your Repo + +Appflow works directly with Git version control and uses your existing code base as the source of truth for Deploy and Package builds. You will first need to integrate with your hosting service, such as GitHub or Bitbucket, or you can push your code directly to Appflow. Once this is completed, Appflow will have access to your code. + +For more on connecting your code repository to Appflow, checkout the [Connect your Repo](https://ionic.io/docs/appflow/quickstart/connect) section inside the Appflow docs. + +## Install the Appflow SDK + +The Appflow SDK (also known as Ionic Deploy plugin) will allow you to take advantage of arguably two of the best Appflow features: deploying live updates to your app and bypassing the app stores. Ionic Appflow's Live Update feature is shipped with Appflow SDK and features the capabilities of detecting and syncing the updates for your app that you have pushed to your identified channels within the dashboard. + +To get the Appflow SDK plugin added to your project, you can follow the install instructions within the Appflow Dashboard by clicking on "Install Instructions" inside of the `Deploy > Destinations` section. Alternatively, you can install the plugin manually by executing the following command in your app's root directory: + +```shell +ionic deploy add \ + --app-id="YOUR_APP_ID" \ + --channel-name="YOUR_CHANNEL_NAME" \ + --update-method="background|auto|none" \ +``` + +For prerequisite and additional instructions on installing the Appflow SDK, visit the [Install the Appflow SDK](https://ionic.io/docs/appflow/quickstart/installation) section inside the Appflow docs. + +## Push a Commit + +In order for Appflow to access the latest and greatest changes to your code, you will need to push a commit via the version control integration of your choosing. For those that use GitHub or Bitbucket, this would look as follows: + +```shell +git add . # stage any changes +git commit -m "added appflow sdk" # commit staged changes +git push origin main # push the changes from the main branch to your git host +``` + +After the push is made, your commit appears under the `Commits` tab of the Appflow Dashboard. For more information, refer to the [Push a Commit](https://ionic.io/docs/appflow/quickstart/push) section inside the Appflow docs. + +## Deploy a Live Update + +With the Appflow SDK installed and your commit pushed up to the Dashboard, you are ready to deploy a live update to a device. The Live Update feature uses the installed Appflow SDK with your native application to listen to a particular Deploy Channel Destination. When a live update is assigned to a Channel Destination, that update will be deployed to user devices running binaries that are configured to listen to that specific Channel Destination. + +To get the live update deployed, a Web build will need to be created. This can be done through the `Start build` icon from the `Commits` tab or by clicking the `New build` button in the top right corner of the `Build > Builds` tab. After selecting the correct commit to deploy, select the `Web` target platform and the `Latest` build stack. Depending on your Appflow plan, you will then be able to include custom environments, if any are configured. Finally, you can enable `Live Update` and pick the Channel to automatically assign the build to once it successfully completes. + +Upon completion of the Web Build, additional versioning options are available to you. After completing this section and you have a successful Deploy build, you can then assign it to the same Channel you configured the Appflow SDK to listen to when you installed it by clicking the `Deploy live updates` button in the build detail page. The same can be done by clicking the `Deploy live updates` icon on the build in the `Build > Builds` tab and select the Channel from the dropdown. + +To receive this live update, you will need to run the app on a device or an emulator. The quickest and easiest way to do this is through the following command: + +```shell +ionic cap run [ios | android] [options] +``` + +Assuming the app is configured correctly to listen to the channel you deployed to, the app should immediately update on startup if you have chosen the auto update method during setup. If the background update method was chosen, be sure to stay in the app for about 30 seconds to ensure the update was downloaded. Then, close the application, reopen it, and the updates are applied! + +To dive into more details on the steps to deploy a live update, as well as additional information such as disabling deploy for development, check out the [Deploy a Live Update](https://ionic.io/docs/appflow/quickstart/deploy) section inside the Appflow docs. + +## Build a Native Binary + +Next up is a native binary for your app build and deploy process. This is done via the [Ionic Package](https://ionic.io/docs/appflow/package/intro) service. First things first, you will need to create a [Package build](https://ionic.io/docs/appflow/package/builds). This can be done by clicking the `Start build` icon from the `Commits` tab or by clicking the `New build` button in the top right from the `Build > Builds` tab. Then you will select the proper commit for your build and fill in all of the several required fields and any optional fields that you want to specify. After filling in all of the information and the build begins, you can check out it's progress and review the logs if you encounter any errors. + +Given a successful Package build, an iOS binary (`.ipa` or IPA) or/and an Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. + +Further information regarding building native binaries can be found inside of the [Build a Native Binary](https://ionic.io/docs/appflow/quickstart/package) section inside the Appflow docs. + +## Create an Automation + +[Automations](https://ionic.io/docs/appflow/automation/intro) enable you and your team to utilize the full CI/CD powers of Appflow. You can create automations that trigger [Package builds](https://ionic.io/docs/appflow/package/builds) and [Deploy builds](https://ionic.io/docs/appflow/deploy/builds) every time your team commits new code to a given branch. The automations can also be configured to use different environments and native configurations for building different versions of your app for development, staging, QA and production. + +For more information, visit the [Create an Automation](https://ionic.io/docs/appflow/quickstart/automation) section within the Appflow docs. That section covers creating a single automation. However, you can create multiple automations for different branches or workflows and customize them to fit your needs. An important note is that the ability to create an automation is available for those on our [Basic plans](https://ionic.io/pricing) and above. + +## Create an Environment + +[Package builds](https://ionic.io/docs/appflow/package/builds) and [Deploy builds](https://ionic.io/docs/appflow/deploy/builds) can be further customized via [Environments](https://ionic.io/docs/appflow/automation/environments). This powerful feature allows you to create different configurations based on the environment variables passed in at build time. When combined with the [Automation](https://ionic.io/docs/appflow/automation/intro) feature, development teams can easily configure development, staging, and production build configurations, allowing them to embrace DevOps best practices and ship better quality updates faster than ever. + +Creating an Environment is available for those on our [Basic plans](https://ionic.io/pricing) and above. More information on this can be found in the [Create an Environment](https://ionic.io/docs/appflow/quickstart/environment) section within the Appflow docs. + +## Create a Native Configuration + +[Native Configurations](https://ionic.io/docs/appflow/package/native-configs) allow you to easily modify common configuration values that can change between different environments (development, production, staging, etc.) so you do not need to use extra logic or manually commit them to version control. Native configurations can be attached to any [Package build](https://ionic.io/docs/appflow/package/intro) or [Automation](https://ionic.io/docs/appflow/automation/intro). + +Native configs can be used to: + +- Overwrite the unique bundle identifier or [id attribute](https://cordova.apache.org/docs/en/latest/config_ref/#widget) in `config.xml` +- Overwrite the App Name as it will appear on the home screen of a device +- Overwrite the [Appflow SDK (Deploy Plugin) variables and preferences](https://ionic.io/docs/appflow/deploy/api#plugin-variables) + +For access to the ability to create a Native Configuration, you will need to be on our [Basic plans](https://ionic.io/pricing) and above. Additional details of this feature can be found in the [Create a Native Configuration](https://ionic.io/docs/appflow/quickstart/native-config) section within the Appflow docs. + +## What’s Next? + +Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to your users' devices! + +There are many paths to follow from here. Try adding another [Ionic UI component](../../components.md) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature, run the build and deploy process again through Appflow to get it out to your users. + +Happy app building! 💙 diff --git a/docs/angular/zoneless.md b/docs/angular/zoneless.md new file mode 100644 index 00000000000..bbcb5031ab5 --- /dev/null +++ b/docs/angular/zoneless.md @@ -0,0 +1,154 @@ +--- +title: Zoneless Change Detection +sidebar_label: Zoneless +--- + + + Zoneless Change Detection with Ionic and Angular + + + +Angular 21 made [zoneless change detection](https://angular.dev/guide/zoneless) the default, removing the dependency on Zone.js. This guide covers what you need to know to run an Ionic Angular app without Zone.js. + +With Zone.js, Angular automatically re-renders after almost any asynchronous task. Without it, Angular only re-renders when you explicitly tell it the view is out of date. Most of your app keeps working unchanged, but a few patterns that relied on Zone.js need a small adjustment. + +## What keeps working automatically + +You do not need to change these. Angular schedules change detection for them in a zoneless app: + +- Template event bindings: `(click)`, `(ionChange)`, `(ionInput)`, and every other `(event)` handler. +- Host listeners (`@HostListener`), including the ones Ionic's form value accessors use, so `[(ngModel)]` and reactive forms stay in sync. +- Signal updates that are read in a template. +- The `async` pipe. +- Ionic page lifecycle hooks (`ionViewWillEnter`, `ionViewDidEnter`, `ionViewWillLeave`, `ionViewDidLeave`) that set state synchronously. Ionic notifies Angular after each hook runs. +- Navigation, route transitions, and tab switching. + +:::note[Angular 22] +Angular 22 also makes `OnPush` the default change detection strategy. Under `OnPush`, synchronous state set as a plain field (including in the lifecycle hooks above) no longer re-renders on its own, even though Ionic notifies Angular. Signals still update the view. For the migration path, refer to the [OnPush Change Detection section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#onpush-change-detection-on-angular-22). +::: + +## What needs a notification + +When you update component state from an asynchronous callback that Angular did not wrap, nothing schedules a re-render. The state changes, but the view does not update. This applies to any Angular code, not only Ionic, and the common sources in an Ionic app are: + +- Awaiting an overlay result, such as `await modal.onWillDismiss()` or `loading.onDidDismiss().then(...)`. +- Asynchronous work started inside a lifecycle hook, for example a `setTimeout` or `fetch` in `ionViewWillEnter`. +- `Platform` event subscriptions (`backButton`, `resize`, `pause`, `resume`, keyboard events) and `Platform.ready()`. +- Any `setTimeout`, `setInterval`, or RxJS subscription that assigns to a component field. + +You can notify Angular in two ways: write to a [signal](https://angular.dev/guide/signals) that the template reads, or inject `ChangeDetectorRef` and call `markForCheck()` after the update. We recommend signals because they work the same with or without Zone.js. + +### Signals (recommended) + +Writing a signal that a template reads schedules change detection automatically, so there is nothing extra to remember after the update. + +```ts +import { Component, inject, signal } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { PickerModal } from './picker.modal'; + +@Component({ + selector: 'app-home', + template: ` + Pick a value +

Selected: {{ selected() }}

+ `, +}) +export class HomePage { + private modalCtrl = inject(ModalController); + readonly selected = signal(undefined); + + async pick() { + const modal = await this.modalCtrl.create({ component: PickerModal }); + await modal.present(); + + const { data } = await modal.onWillDismiss(); + // Writing the signal updates the view automatically. + this.selected.set(data); + } +} +``` + +### `ChangeDetectorRef.markForCheck()` + +If you are not using signals for a particular piece of state, inject `ChangeDetectorRef` and call `markForCheck()` after the asynchronous update. It is a no-op-or-better under Zone.js, so it is safe to leave in place if you later re-enable zones. + +```ts +import { ChangeDetectorRef, Component, inject } from '@angular/core'; + +@Component({ + selector: 'app-list', + template: `@for (item of items; track item) { {{ item }} }`, +}) +export class ListPage { + private cdr = inject(ChangeDetectorRef); + items: string[] = []; + + ionViewWillEnter() { + // Asynchronous work inside a lifecycle hook still needs a notification. + setTimeout(() => { + this.items = ['A', 'B', 'C']; + this.cdr.markForCheck(); + }, 1000); + } +} +``` + +## Common Ionic patterns + +These apply the two approaches above to patterns you are likely to hit in an Ionic app. + +### Inline overlays with dynamic content + +Content projected into an inline `ion-modal` or `ion-popover` follows the same rule. If you populate it asynchronously, update a signal or call `markForCheck()`: + +```ts +@Component({ + selector: 'app-inline', + template: ` + + + + @for (item of items(); track item) { + {{ item }} + } + + + + `, +}) +export class InlinePage { + readonly items = signal([]); + + open(popover: IonPopover) { + popover.present(); + setTimeout(() => this.items.set(['A', 'B', 'C', 'D']), 1000); + } +} +``` + +Inline overlays also expose their events as outputs (for example `ionModalDidDismiss`), which you can convert to a signal with [`toSignal`](https://angular.dev/api/core/rxjs-interop/toSignal) if you prefer a reactive style. + +### Platform events + +`Platform` exposes its events as RxJS subjects. Update a signal inside the subscription so the view reflects the change: + +```ts +export class AppComponent { + private platform = inject(Platform); + readonly isLandscape = signal(false); + + constructor() { + this.platform.resize.subscribe(() => { + this.isLandscape.set(this.platform.isLandscape()); + }); + } +} +``` + +## Staying on Zone.js + +If you are not ready to adopt zoneless change detection, you can opt back into Zone.js with `provideZoneChangeDetection()`. Refer to the [Keeping Zone.js section of the Ionic 9 upgrade guide](/docs/updating/9-0.md#keeping-zonejs) for the exact configuration. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000000..45e4c1d8d19 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,17 @@ +--- +title: API Index +--- + +import APIList from '@components/page/api/APIList'; + + + API Index | Ionic Docs API Index for all API Custom Elements + + + +Each Ionic [component](/docs/components) consists of one or more [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). Each custom element, in turn, may expose properties, methods, events, and CSS custom properties. + + diff --git a/docs/api/accordion-group.md b/docs/api/accordion-group.md new file mode 100644 index 00000000000..79783a6d71e --- /dev/null +++ b/docs/api/accordion-group.md @@ -0,0 +1,59 @@ +--- +title: "ion-accordion-group" +--- +import Props from '@ionic-internal/component-api/v9/accordion-group/props.md'; +import Events from '@ionic-internal/component-api/v9/accordion-group/events.md'; +import Methods from '@ionic-internal/component-api/v9/accordion-group/methods.md'; +import Parts from '@ionic-internal/component-api/v9/accordion-group/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/accordion-group/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/accordion-group/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Accordion group is a container for accordion instances. It manages the state of the accordions and provides keyboard navigation. + +Refer to the [Accordion](./accordion) documentation for more information. + + +## Interfaces + +### AccordionGroupChangeEventDetail + +```typescript +interface AccordionGroupChangeEventDetail { + value: T; +} +``` + +### AccordionGroupCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing with Ionic events emitted from this component. + +```typescript +interface AccordionGroupCustomEvent extends CustomEvent { + detail: AccordionGroupChangeEventDetail; + target: HTMLIonAccordionGroupElement; +} +``` + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/accordion.md b/docs/api/accordion.md new file mode 100644 index 00000000000..9040bed140f --- /dev/null +++ b/docs/api/accordion.md @@ -0,0 +1,217 @@ +--- +title: "ion-accordion" +--- +import Props from '@ionic-internal/component-api/v9/accordion/props.md'; +import Events from '@ionic-internal/component-api/v9/accordion/events.md'; +import Methods from '@ionic-internal/component-api/v9/accordion/methods.md'; +import Parts from '@ionic-internal/component-api/v9/accordion/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/accordion/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/accordion/slots.md'; + + + ion-accordion: Accordion Components: How to Build & Examples + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +Accordions provide collapsible sections in your content to reduce vertical space while providing a way of organizing and grouping information. All `ion-accordion` components should be grouped inside `ion-accordion-group` components. + +## Basic Usage + +import Basic from '@site/static/usage/v9/accordion/basic/index.md'; + + + +## Toggle Accordions + +Which accordion is open is controlled by setting the `value` property on `ion-accordion-group`. Setting this property allows developers to programmatically expand or collapse certain accordions. + +import Toggle from '@site/static/usage/v9/accordion/toggle/index.md'; + + + +## Listen for Accordion State Changes + +:::caution +Most `ionChange` events emitted by other components such as [Input](./input) and [Textarea](./textarea) bubble. As a result, these events will bubble up and cause your `ionChange` listener on the Accordion Group to fire if the associated components are used inside of an Accordion. + +When using other components that emit `ionChange` inside of Accordion it is recommended to have the `ionChange` callback on Accordion Group check the `target` key on the event passed to the callback to verify that `ionChange` is coming from the Accordion Group and not any descendants. +::: + +Developers can listen for the `ionChange` event to be notified when accordions expand or collapse. + +import ListenChanges from '@site/static/usage/v9/accordion/listen-changes/index.md'; + + + +## Multiple Accordions + +Developers can allow multiple accordions to be open at once with the `multiple` property. + +import Multiple from '@site/static/usage/v9/accordion/multiple/index.md'; + + + +## Disabling Accordions + +### Individual Accordion + +Individual accordions can be disabled with the `disabled` property on `ion-accordion`. + +import DisableIndividual from '@site/static/usage/v9/accordion/disable/individual/index.md'; + + + +### Accordion Group + +The accordion group can be disabled with the `disabled` property on `ion-accordion-group`. + +import DisableGroup from '@site/static/usage/v9/accordion/disable/group/index.md'; + + + +## Readonly Accordions + +### Individual Accordion + +Individual accordions can be disabled with the `readonly` property on `ion-accordion`. + +import ReadonlyIndividual from '@site/static/usage/v9/accordion/readonly/individual/index.md'; + + + +### Accordion Group + +The accordion group can be disabled with the `readonly` property on `ion-accordion-group`. + +import ReadonlyGroup from '@site/static/usage/v9/accordion/readonly/group/index.md'; + + + +## Anatomy + +### Header + +The `header` slot is used as the toggle that will expand or collapse your accordion. We recommend you use an `ion-item` here to take advantage of the accessibility and theming functionalities. + +When using `ion-item` in the `header` slot, the `ion-item`'s `button` prop is set to `true` and the `detail` prop is set to `false`. In addition, we will also automatically add a toggle icon to the `ion-item`. This icon will automatically be rotated when you expand or collapse the accordion. Refer to [Customizing Icons](#icons) for more information. + +### Content + +The `content` slot is used as the part of the accordion that is revealed or hidden depending on the state of your accordion. You can place anything here except for another `ion-content` instance as only one instance of `ion-content` should be added per page. + +## Customization + +### Expansion Styles + +There are two built in expansion styles: `compact` and `inset`. This expansion style is set via the `expand` property on `ion-accordion-group`. + +When `expand="inset"`, the accordion group is given a border radius. On `md` mode, the entire accordion will shift down when it is opened. + +import ExpansionStyles from '@site/static/usage/v9/accordion/customization/expansion-styles/index.md'; + + + +### Advanced Expansion Styles + +You can customize the expansion behavior by styling based on the accordion's state. There are four state classes applied to `ion-accordion`. Styling using these classes can allow you to create advanced state transitions: + +| Class Name | Description | +| ---------- | ----------- | +| `.accordion-expanding` | Applied when the accordion is actively expanding | +| `.accordion-expanded` | Applied when the accordion is fully expanded | +| `.accordion-collapsing` | Applied when the accordion is actively collapsing | +| `.accordion-collapsed` | Applied when the accordion is fully collapsed | + +If you need to target specific pieces of the accordion, we recommend targeting the element directly. For example, if you want to customize the ion-item in your header slot when the accordion is expanded, you can use the following selector: + +```css +ion-accordion.accordion-expanding ion-item[slot="header"], +ion-accordion.accordion-expanded ion-item[slot="header"] { + --color: red; +} +``` + +import AdvancedExpansionStyles from '@site/static/usage/v9/accordion/customization/advanced-expansion-styles/index.md'; + + + +### Icons + +When using an `ion-item` in the `header` slot, we automatically add an `ion-icon`. The type of icon used can be controlled by the `toggleIcon` property, and the slot it is added to can be controlled with the `toggleIconSlot` property. + +If you would like to manage the icon yourself or use an icon that is not an `ion-icon`, you can add the `ion-accordion-toggle-icon` class to the icon element. + +Regardless of which option you choose, the icon will automatically be rotated when you expand or collapse the accordion. + +import Icons from '@site/static/usage/v9/accordion/customization/icons/index.md'; + + + +### Theming + +Since `ion-accordion` acts as a shell around the header and content elements, you can easily theme the accordion however you would like. You can theme the header by targeting the slotted `ion-item`. Since you are using `ion-item`, you also have access to all of the [ion-item CSS Variables](./item#css-custom-properties) and [ion-item Shadow Parts](./item#css-shadow-parts). Theming the content is also easily achieved by targeting the element that is in the `content` slot. + +import Theming from '@site/static/usage/v9/accordion/customization/theming/index.md'; + + + +## Accessibility + +### Animations + +By default, animations are enabled when expanding or collapsing an accordion item. Animations will be automatically disabled when the `prefers-reduced-motion` media query is supported and set to `reduce`. For browsers that do not support this, animations can be disabled by setting the `animated` config in your Ionic Framework app. + +import AccessibilityAnimations from '@site/static/usage/v9/accordion/accessibility/animations/index.md'; + + + +### Keyboard Interactions + +When used inside an `ion-accordion-group`, `ion-accordion` has full keyboard support for interacting with the component. The following table details what each key does: + +| Key | Description | +| ------------------------------------ | ------------------------------------------------------------ | +| Space or Enter | When focus is on the accordion header, the accordion will collapse or expand depending on the state of the component. | +| Tab | Moves focus to the next focusable element. | +| Shift + Tab | Moves focus to the previous focusable element. | +| Down Arrow | - When focus is on an accordion header, moves focus to the next accordion header.
- When focus is on the last accordion header, moves focus to the first accordion header. | +| Up Arrow | - When focus is on an accordion header, moves focus to the previous accordion header.
- When focus is on the first accordion header, moves focus to the last accordion header. | +| Home | When focus is on an accordion header, moves focus to the first accordion header. | +| End | When focus is on an accordion header, moves focus to the last accordion header. | + +## Performance + +### Animations + +The accordion animation works by knowing the height of the `content` slot when the animation starts. The accordion expects that this height will remain consistent throughout the animation. As a result, developers should avoid performing any operation that may change the height of the content during the animation. + +For example, lazily loading images may cause layout shifts as they load. As the animation plays, a lazily loaded image (such as a native `` with `loading="lazy"`) loads its data and changes its dimensions to fit, which can change the height of the `content` slot. Developers have a few options for avoiding this: + +1. Load images eagerly by omitting `loading="lazy"`. An `` does not lazy load by default, so the image loads up front instead of during the animation. This is the simplest option and works well if you have small images that do not significantly benefit from lazy loading. + +2. Reserve space for the image ahead of time. If you need lazy loading and know the dimensions of the images (such as loading icons of the same size), set a width and height on the `` using its attributes or CSS. This gives you the benefit of lazy loading while avoiding layout shifts. + +3. If neither of these options are applicable, developers may want to consider disabling animations altogether by using the `animated` property on [ion-accordion-group](./accordion-group). + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/action-sheet.md b/docs/api/action-sheet.md new file mode 100644 index 00000000000..13eebe6934e --- /dev/null +++ b/docs/api/action-sheet.md @@ -0,0 +1,298 @@ +--- +title: "ion-action-sheet" +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import Props from '@ionic-internal/component-api/v9/action-sheet/props.md'; +import Events from '@ionic-internal/component-api/v9/action-sheet/events.md'; +import Methods from '@ionic-internal/component-api/v9/action-sheet/methods.md'; +import Parts from '@ionic-internal/component-api/v9/action-sheet/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/action-sheet/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/action-sheet/slots.md'; + + + ion-action-sheet: Action Sheet Dialog for iOS and Android + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +An Action Sheet is a dialog that displays a set of options. It appears on top of the app's content, and must be manually dismissed by the user before they can resume interaction with the app. Destructive options are made obvious in `ios` mode. There are multiple ways to dismiss the action sheet, including tapping the backdrop or hitting the escape key on desktop. + +## Inline Action Sheets (Recommended) + +`ion-action-sheet` can be used by writing the component directly in your template. This reduces the number of handlers you need to wire up in order to present the Action Sheet. + +import Trigger from '@site/static/usage/v9/action-sheet/inline/trigger/index.md'; + + + +### Using `isOpen` + +The `isOpen` property on `ion-action-sheet` allows developers to control the presentation state of the Action Sheet from their application state. This means when `isOpen` is set to `true` the Action Sheet will be presented, and when `isOpen` is set to `false` the Action Sheet will be dismissed. + +`isOpen` uses a one-way data binding, meaning it will not automatically be set to `false` when the Action Sheet is dismissed. Developers should listen for the `ionActionSheetDidDismiss` or `didDismiss` event and set `isOpen` to `false`. The reason for this is it prevents the internals of `ion-action-sheet` from being tightly coupled with the state of the application. With a one way data binding, the Action Sheet only needs to concern itself with the boolean value that the reactive variable provides. With a two way data binding, the Action Sheet needs to concern itself with both the boolean value as well as the existence of the reactive variable itself. This can lead to non-deterministic behaviors and make applications harder to debug. + +import IsOpen from '@site/static/usage/v9/action-sheet/inline/isOpen/index.md'; + + + +## Controller Action Sheets + +The `actionSheetController` can be used in situations where more control is needed over when the Action Sheet is presented and dismissed. + +import Controller from '@site/static/usage/v9/action-sheet/controller/index.md'; + + + +## Buttons + +A button's `role` property can either be `destructive` or `cancel`. Buttons without a role property will have the default look for the platform. Buttons with the `cancel` role will always load as the bottom button, no matter where they are in the array. All other buttons will be displayed in the order they have been added to the `buttons` array. Note: We recommend that `destructive` buttons are always the first button in the array, making them the top button. Additionally, if the action sheet is dismissed by tapping the backdrop, then it will fire the handler from the button with the cancel role. + +A button can also be passed data via the `data` property on `ActionSheetButton`. This will populate the `data` field in the return value of the `onDidDismiss` method. + +## Collecting Role Information on Dismiss + +When the `didDismiss` event is fired, the `data` and `role` fields of the event detail can be used to gather information about how the Action Sheet was dismissed. + +import RoleInfo from '@site/static/usage/v9/action-sheet/role-info-on-dismiss/index.md'; + + + +## Theming + +Action Sheet uses scoped encapsulation, which means it will automatically scope its CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector. + +### Styling + +We recommend passing a custom class to `cssClass` in the `create` method and using that to add custom styles to the host and inner elements. This property can also accept multiple classes separated by spaces. + +```css +/* DOES NOT WORK - not specific enough */ +.action-sheet-group { + background: #e5e5e5; +} + +/* Works - pass "my-custom-class" in cssClass to increase specificity */ +.my-custom-class .action-sheet-group { + background: #e5e5e5; +} +``` + +import Styling from '@site/static/usage/v9/action-sheet/theming/styling/index.md'; + + + +### CSS Custom Properties + +Any of the defined [CSS Custom Properties](#css-custom-properties-1) can be used to style the Action Sheet without needing to target individual elements. + +import CssCustomProperties from '@site/static/usage/v9/action-sheet/theming/css-properties/index.md'; + + + +## Accessibility + +### Screen Readers + +Action Sheets set aria properties in order to be [accessible](../reference/glossary#a11y) to screen readers, but these properties can be overridden if they aren't descriptive enough or don't align with how the action sheet is being used in an app. + +#### Role + +Action Sheets are given a `role` of [`dialog`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role). In order to align with the ARIA spec, either the `aria-label` or `aria-labelledby` attribute must be set. + +#### Action Sheet Description + +It is strongly recommended that every Action Sheet have the `header` property defined, as Ionic will automatically set `aria-labelledby` to point to the header element. However, if you choose not to include a `header`, an alternative is to use the `htmlAttributes` property to provide a descriptive `aria-label` or set a custom `aria-labelledby` value. + + + + + +```javascript +const actionSheet = await this.actionSheetController.create({ + htmlAttributes: { + 'aria-label': 'action sheet dialog', + }, +}); +``` + + + + + +```javascript +const actionSheet = await this.actionSheetController.create({ + htmlAttributes: { + 'aria-label': 'action sheet dialog', + }, +}); +``` + + + + + +```javascript +useIonActionSheet({ + htmlAttributes: { + 'aria-label': 'action sheet dialog', + }, +}); +``` + + + + + +```javascript +const actionSheet = await actionSheetController.create({ + htmlAttributes: { + 'aria-label': 'action sheet dialog', + }, +}); +``` + + + + + +#### Action Sheet Buttons Description + +Buttons containing text will be read by a screen reader. If a button contains only an icon, or a description other than the existing text is desired, a label should be assigned to the button by passing `aria-label` to the `htmlAttributes` property on the button. + + + + + +```javascript +const actionSheet = await this.actionSheetController.create({ + header: 'Header', + buttons: [ + { + icon: 'close', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +const actionSheet = await this.actionSheetController.create({ + header: 'Header', + buttons: [ + { + icon: 'close', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +useIonActionSheet({ + header: 'Header', + buttons: [ + { + icon: 'close', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +const actionSheet = await actionSheetController.create({ + header: 'Header', + buttons: [ + { + icon: 'close', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +## Interfaces + +### ActionSheetButton + +```typescript +interface ActionSheetButton { + text?: string; + role?: 'cancel' | 'destructive' | 'selected' | string; + icon?: string; + cssClass?: string | string[]; + id?: string; + htmlAttributes?: { [key: string]: any }; + handler?: () => boolean | void | Promise; + data?: T; +} +``` + +### ActionSheetOptions + +```typescript +interface ActionSheetOptions { + header?: string; + subHeader?: string; + cssClass?: string | string[]; + buttons: (ActionSheetButton | string)[]; + backdropDismiss?: boolean; + translucent?: boolean; + animated?: boolean; + mode?: Mode; + keyboardClose?: boolean; + id?: string; + htmlAttributes?: { [key: string]: any }; + + enterAnimation?: AnimationBuilder; + leaveAnimation?: AnimationBuilder; +} +``` + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/alert.md b/docs/api/alert.md new file mode 100644 index 00000000000..ec64807640d --- /dev/null +++ b/docs/api/alert.md @@ -0,0 +1,350 @@ +--- +title: "ion-alert" +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import Props from '@ionic-internal/component-api/v9/alert/props.md'; +import Events from '@ionic-internal/component-api/v9/alert/events.md'; +import Methods from '@ionic-internal/component-api/v9/alert/methods.md'; +import Parts from '@ionic-internal/component-api/v9/alert/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/alert/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/alert/slots.md'; + + + ion-alert: Ionic Alert Buttons with Custom Message Prompts + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +An Alert is a dialog that presents users with information or collects information from the user using inputs. An alert appears on top of the app's content, and must be manually dismissed by the user before they can resume interaction with the app. It can also optionally have a `header`, `subHeader` and `message`. + +## Inline Alerts (Recommended) + +`ion-alert` can be used by writing the component directly in your template. This reduces the number of handlers you need to wire up in order to present the Alert. + +import Trigger from '@site/static/usage/v9/alert/presenting/trigger/index.md'; + + + +### Using `isOpen` + +The `isOpen` property on `ion-alert` allows developers to control the presentation state of the Alert from their application state. This means when `isOpen` is set to `true` the Alert will be presented, and when `isOpen` is set to `false` the Alert will be dismissed. + +`isOpen` uses a one-way data binding, meaning it will not automatically be set to `false` when the Alert is dismissed. Developers should listen for the `ionAlertDidDismiss` or `didDismiss` event and set `isOpen` to `false`. The reason for this is it prevents the internals of `ion-alert` from being tightly coupled with the state of the application. With a one way data binding, the Alert only needs to concern itself with the boolean value that the reactive variable provides. With a two way data binding, the Alert needs to concern itself with both the boolean value as well as the existence of the reactive variable itself. This can lead to non-deterministic behaviors and make applications harder to debug. + +import IsOpen from '@site/static/usage/v9/alert/presenting/isOpen/index.md'; + + + +## Controller Alerts + +The `alertController` can be used in situations where more control is needed over when the Alert is presented and dismissed. + +import Controller from '@site/static/usage/v9/alert/presenting/controller/index.md'; + + + +## Buttons + +In the array of `buttons`, each button includes properties for its `text`, and optionally a `handler`. If a handler returns `false` then the alert will not automatically be dismissed when the button is clicked. All buttons will show up in the order they have been added to the `buttons` array from left to right. Note: The right most button (the last one in the array) is the main button. + +Optionally, a `role` property can be added to a button, such as `cancel`. If a `cancel` role is on one of the buttons, then if the alert is dismissed by tapping the backdrop, then it will fire the handler from the button with a cancel role. + +import Buttons from '@site/static/usage/v9/alert/buttons/index.md'; + + + + +## Inputs + +Alerts can also include several different inputs whose data can be passed back to the app. Inputs can be used as a simple way to prompt users for information. Radios, checkboxes and text inputs are all accepted, but they cannot be mixed. For example, an alert could have all radio button inputs, or all checkbox inputs, but the same alert cannot mix radio and checkbox inputs. Do note however, different types of "text" inputs can be mixed, such as `url`, `email`, `text`, `textarea` etc. If you require a complex form UI which doesn't fit within the guidelines of an alert then we recommend building the form within a modal instead. + +### Text Inputs Example + +import TextInputs from '@site/static/usage/v9/alert/inputs/text-inputs/index.md'; + + + +### Radio Example + +import Radios from '@site/static/usage/v9/alert/inputs/radios/index.md'; + + + +## Customization + +Alert uses scoped encapsulation, which means it will automatically scope its CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector. + +We recommend passing a custom class to `cssClass` in the `create` method and using that to add custom styles to the host and inner elements. This property can also accept multiple classes separated by spaces. + +```css +/* DOES NOT WORK - not specific enough */ +.alert-wrapper { + background: #e5e5e5; +} + +/* Works - pass "my-custom-class" in cssClass to increase specificity */ +.my-custom-class .alert-wrapper { + background: #e5e5e5; +} +``` + +Any of the defined [CSS Custom Properties](#css-custom-properties) can be used to style the Alert without needing to target individual elements: + +```css +.my-custom-class { + --background: #e5e5e5; +} +``` + +import Customization from '@site/static/usage/v9/alert/customization/index.md'; + + + +:::note + If you are building an Ionic Angular app, the styles need to be added to a global stylesheet file. +::: + +## Accessibility + +### Screen Readers + +Alerts set aria properties in order to be [accessible](../reference/glossary#a11y) to screen readers, but these properties can be overridden if they aren't descriptive enough or don't align with how the alert is being used in an app. + +#### Role + +Ionic automatically sets the Alert's `role` to either [`alertdialog`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alertdialog_role) if there are any inputs or buttons included, or [`alert`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role) if there are none. + +#### Alert Description + +If the `header` property is defined for the Alert, the `aria-labelledby` attribute will be automatically set to the header's ID. The `subHeader` element will be used as a fallback if `header` is not defined. Similarly, the `aria-describedby` attribute will be automatically set to the ID of the `message` element if that property is defined. + +It is strongly recommended that your Alert have a `message`, as well as either a `header` or `subHeader`, in order to align with the ARIA spec. If you choose not to include a `header` or `subHeader`, an alternative is to provide a descriptive `aria-label` using the `htmlAttributes` property. + + + + + +```javascript +const alert = await this.alertController.create({ + message: 'This is an alert with custom aria attributes.', + htmlAttributes: { + 'aria-label': 'alert dialog', + }, +}); +``` + + + + + +```javascript +const alert = await this.alertController.create({ + message: 'This is an alert with custom aria attributes.', + htmlAttributes: { + 'aria-label': 'alert dialog', + }, +}); +``` + + + + + +```javascript +useIonAlert({ + message: 'This is an alert with custom aria attributes.', + htmlAttributes: { + 'aria-label': 'alert dialog', + }, +}); +``` + + + + + +```javascript +const alert = await alertController.create({ + message: 'This is an alert with custom aria attributes.', + htmlAttributes: { + 'aria-label': 'alert dialog', + }, +}); +``` + + + + + + +All ARIA attributes can be manually overwritten by defining custom values in the `htmlAttributes` property of the Alert. + +#### Alert Buttons Description + +Buttons containing text will be read by a screen reader. If a description other than the existing text is desired, a label can be set on the button by passing `aria-label` to the `htmlAttributes` property on the button. + + + + + +```javascript +const alert = await this.alertController.create({ + header: 'Header', + buttons: [ + { + text: 'Exit', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +const alert = await this.alertController.create({ + header: 'Header', + buttons: [ + { + text: 'Exit', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +useIonAlert({ + header: 'Header', + buttons: [ + { + text: 'Exit', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +```javascript +const alert = await alertController.create({ + header: 'Header', + buttons: [ + { + text: 'Exit', + htmlAttributes: { + 'aria-label': 'close', + }, + }, + ], +}); +``` + + + + + +## Interfaces + +### AlertButton + +```typescript +type AlertButtonOverlayHandler = boolean | void | { [key: string]: any }; + +interface AlertButton { + text: string; + role?: 'cancel' | 'destructive' | string; + cssClass?: string | string[]; + id?: string; + htmlAttributes?: { [key: string]: any }; + handler?: (value: any) => AlertButtonOverlayHandler | Promise; +} +``` + + +### AlertInput + +```typescript +interface AlertInput { + type?: TextFieldTypes | 'checkbox' | 'radio' | 'textarea'; + name?: string; + placeholder?: string; + value?: any; + /** + * The label text to display next to the input, if the input type is `radio` or `checkbox`. + */ + label?: string; + checked?: boolean; + disabled?: boolean; + id?: string; + handler?: (input: AlertInput) => void; + min?: string | number; + max?: string | number; + cssClass?: string | string[]; + attributes?: { [key: string]: any }; + tabindex?: number; +} +``` + + +### AlertOptions + +```typescript +interface AlertOptions { + header?: string; + subHeader?: string; + message?: string | IonicSafeString; + cssClass?: string | string[]; + inputs?: AlertInput[]; + buttons?: (AlertButton | string)[]; + backdropDismiss?: boolean; + translucent?: boolean; + animated?: boolean; + htmlAttributes?: { [key: string]: any }; + + mode?: Mode; + keyboardClose?: boolean; + id?: string; + + enterAnimation?: AnimationBuilder; + leaveAnimation?: AnimationBuilder; +} +``` + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/app.md b/docs/api/app.md new file mode 100644 index 00000000000..227ddfd8ad3 --- /dev/null +++ b/docs/api/app.md @@ -0,0 +1,53 @@ +--- +title: "ion-app" +--- +import Props from '@ionic-internal/component-api/v9/app/props.md'; +import Events from '@ionic-internal/component-api/v9/app/events.md'; +import Methods from '@ionic-internal/component-api/v9/app/methods.md'; +import Parts from '@ionic-internal/component-api/v9/app/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/app/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/app/slots.md'; + + + ion-app: Container Element for an Ionic Application + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + +App is a container element for an Ionic application. There should only be one `` element per project. An app can have many Ionic components including menus, headers, content, and footers. The overlay components get appended to the `` when they are presented. + +Using `ion-app` enables the following behaviors: + +* [Keyboard Lifecycle Events](../developing/keyboard#keyboard-lifecycle-events) without the need for any native plugins +* [Hardware Back Button Listeners](../developing/hardware-back-button) for customizing the hardware back button behavior on Android devices +* Status bar support in Capacitor or Cordova which allows users to scroll to the top of the view by tapping the status bar +* Scroll assist utilities which scroll the content so focused text inputs are not covered by the on-screen keyboard +* [Ripple effect](./ripple-effect) when activating buttons on Material Design mode +* Other tap and focus utilities which make the experience of using an Ionic app feel more native + +## Programmatic Focus + +Ionic offers focus utilities for components with the `ion-focusable` class. These utilities automatically manage focus for components when certain keyboard keys, like Tab, are pressed. Components can also be programmatically focused in response to user actions using the `setFocus` method from `ion-app`. + +import SetFocus from '@site/static/usage/v9/app/set-focus/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/avatar.md b/docs/api/avatar.md new file mode 100644 index 00000000000..2f25ffed025 --- /dev/null +++ b/docs/api/avatar.md @@ -0,0 +1,67 @@ +--- +title: "ion-avatar" +--- + +import Props from '@ionic-internal/component-api/v9/avatar/props.md'; +import Events from '@ionic-internal/component-api/v9/avatar/events.md'; +import Methods from '@ionic-internal/component-api/v9/avatar/methods.md'; +import Parts from '@ionic-internal/component-api/v9/avatar/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/avatar/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/avatar/slots.md'; + + + ion-avatar: Circular Application Avatar Icon Component + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Avatars are circular components that usually wrap an image or icon. They can be used to represent a person or an object. + +Avatars can be used by themselves or inside of any element. If placed inside of an `ion-chip` or `ion-item`, the avatar will resize to fit the parent component. To position an avatar on the left or right side of an item, set the slot to `start` or `end`, respectively. + +## Basic Usage + +import Basic from '@site/static/usage/v9/avatar/basic/index.md'; + + + +## Chip Avatar + +import Chip from '@site/static/usage/v9/avatar/chip/index.md'; + + + +## Item Avatar + +import Item from '@site/static/usage/v9/avatar/item/index.md'; + + + +## Theming + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/avatar/theming/css-properties/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/back-button.md b/docs/api/back-button.md new file mode 100644 index 00000000000..6165b426ea4 --- /dev/null +++ b/docs/api/back-button.md @@ -0,0 +1,57 @@ +--- +title: "ion-back-button" +--- +import Props from '@ionic-internal/component-api/v9/back-button/props.md'; +import Events from '@ionic-internal/component-api/v9/back-button/events.md'; +import Methods from '@ionic-internal/component-api/v9/back-button/methods.md'; +import Parts from '@ionic-internal/component-api/v9/back-button/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/back-button/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/back-button/slots.md'; + + + ion-back-button: Custom Menu Back Button for Applications + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +The back button navigates back in the app's history when clicked. It is only displayed when there is history in the navigation stack, unless [`defaultHref`](#default-back-history) is set. The back button displays different text and icon based on the mode, but this can be customized. + +## Basic Usage + +import Basic from '@site/static/usage/v9/back-button/basic/index.md'; + + + +## Custom Back Button + +By default, the back button will display the text `"Back"` with a `"chevron-back"` icon on `ios`, and an `"arrow-back-sharp"` icon on `md`. This can be customized per back button component by setting the `icon` or `text` properties. Alternatively, it can be set globally using the `backButtonIcon` or `backButtonText` properties in the global config. Refer to the [Config docs](../developing/config) for more information. + +import Custom from '@site/static/usage/v9/back-button/custom/index.md'; + + + +## Default Back History + +Occasionally an app may need to show the back button and navigate back when there is no history. This can be done by setting the `defaultHref` on the back button to a path. In order to use `defaultHref`, the app must contain a router with paths set. + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/backdrop.md b/docs/api/backdrop.md new file mode 100644 index 00000000000..dffb3ce393d --- /dev/null +++ b/docs/api/backdrop.md @@ -0,0 +1,51 @@ +--- +title: "ion-backdrop" +--- +import Props from '@ionic-internal/component-api/v9/backdrop/props.md'; +import Events from '@ionic-internal/component-api/v9/backdrop/events.md'; +import Methods from '@ionic-internal/component-api/v9/backdrop/methods.md'; +import Parts from '@ionic-internal/component-api/v9/backdrop/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/backdrop/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/backdrop/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Backdrops are full screen components that overlay other components. They are useful behind components that transition in on top of other content and can be used to dismiss that component. + +## Basic Usage + +The backdrop prevents clicking or tapping on the content behind it. It is transparent by default, so the below demo includes CSS to make it visible. + +import Basic from '@site/static/usage/v9/backdrop/basic/index.md'; + + + +## Styling + +The backdrop can be customized by assigning CSS properties directly to the backdrop element. Common properties include `background-color`, `background` and `opacity`. + +Content can be displayed above the backdrop by setting a `z-index` on the content, higher than the backdrop (defaults to `2`). + +import Styling from '@site/static/usage/v9/backdrop/styling/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + \ No newline at end of file diff --git a/docs/api/badge.md b/docs/api/badge.md new file mode 100644 index 00000000000..62e1c5034d9 --- /dev/null +++ b/docs/api/badge.md @@ -0,0 +1,70 @@ +--- +title: "ion-badge" +--- +import Props from '@ionic-internal/component-api/v9/badge/props.md'; +import Events from '@ionic-internal/component-api/v9/badge/events.md'; +import Methods from '@ionic-internal/component-api/v9/badge/methods.md'; +import Parts from '@ionic-internal/component-api/v9/badge/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/badge/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/badge/slots.md'; + + + ion-badge: iOS & Android App Notification Badge Icons + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Badges are inline block elements that usually appear near another element. Typically they contain a number or other characters. They can be used as a notification that there are additional items associated with an element and indicate how many items there are. Badges are hidden if no content is passed in. + +## Basic Usage + +import Basic from '@site/static/usage/v9/badge/basic/index.md'; + + + +## Badges in Tab Buttons + +Badges can be added inside a tab button, often used to indicate notifications or highlight additional items associated with the element. + +:::info +Empty badges are only available for `md` mode. +::: + +import InsideTabBar from '@site/static/usage/v9/badge/inside-tab-bar/index.md'; + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/badge/theming/colors/index.md'; + + + +### CSS Properties + +import CSSProps from '@site/static/usage/v9/badge/theming/css-properties/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/breadcrumb.md b/docs/api/breadcrumb.md new file mode 100644 index 00000000000..b33712a34bb --- /dev/null +++ b/docs/api/breadcrumb.md @@ -0,0 +1,60 @@ +--- +title: "ion-breadcrumb" +--- +import Props from '@ionic-internal/component-api/v9/breadcrumb/props.md'; +import Events from '@ionic-internal/component-api/v9/breadcrumb/events.md'; +import Methods from '@ionic-internal/component-api/v9/breadcrumb/methods.md'; +import Parts from '@ionic-internal/component-api/v9/breadcrumb/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/breadcrumb/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/breadcrumb/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +A Breadcrumb is a single navigation item that is a child of the Breadcrumbs component. A breadcrumb can link elsewhere in an app or it can be plain text. Each breadcrumb has a separator between it and the next breadcrumb and can optionally contain an icon. + +Refer to the [Breadcrumbs](./breadcrumbs) documentation for more information. + +## Interfaces + +### BreadcrumbCollapsedClickEventDetail + +```typescript +interface BreadcrumbCollapsedClickEventDetail { + collapsedBreadcrumbs?: HTMLIonBreadcrumbElement[]; +} +``` + +### BreadcrumbCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing . + +```typescript +interface BreadcrumbCustomEvent extends CustomEvent { + detail: BreadcrumbCollapsedClickEventDetail; + target: HTMLIonBreadcrumbElement; +} +``` + + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/breadcrumbs.md b/docs/api/breadcrumbs.md new file mode 100644 index 00000000000..313de4f38aa --- /dev/null +++ b/docs/api/breadcrumbs.md @@ -0,0 +1,104 @@ +--- +title: "ion-breadcrumbs" +--- +import Props from '@ionic-internal/component-api/v9/breadcrumbs/props.md'; +import Events from '@ionic-internal/component-api/v9/breadcrumbs/events.md'; +import Methods from '@ionic-internal/component-api/v9/breadcrumbs/methods.md'; +import Parts from '@ionic-internal/component-api/v9/breadcrumbs/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/breadcrumbs/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/breadcrumbs/slots.md'; + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Breadcrumbs are navigation items that are used to indicate where a user is on an app or site. They should be used for large sites and apps with hierarchically arranged pages. Breadcrumbs can be collapsed based on the maximum number that can show, and the collapsed indicator can be clicked on to present a popover with more information or expand the collapsed breadcrumbs. + +## Basic Usage + +import Basic from '@site/static/usage/v9/breadcrumbs/basic/index.md'; + + + +## Using Icons + +### Icons on Items + +import IconsOnItems from '@site/static/usage/v9/breadcrumbs/icons/icons-on-items/index.md'; + + + +### Custom Separators + +import CustomSeparators from '@site/static/usage/v9/breadcrumbs/icons/custom-separators/index.md'; + + + +## Collapsing Items + +### Max Items + +If there are more items than the value of `maxItems`, the breadcrumbs will be collapsed. By default, only the first and last items will be shown. + +import MaxItems from '@site/static/usage/v9/breadcrumbs/collapsing-items/max-items/index.md'; + + + +### Items Before or After Collapse + +Once the items are collapsed, the number of items to show can be controlled by the `itemsBeforeCollapse` and `itemsAfterCollapse` properties. + +import ItemsBeforeAfter from '@site/static/usage/v9/breadcrumbs/collapsing-items/items-before-after/index.md'; + + + +### Collapsed Indicator Click -- Expand Breadcrumbs + +Clicking the collapsed indicator will fire the `ionCollapsedClick` event. This can be used to, for example, expand the breadcrumbs. + +import ExpandOnClick from '@site/static/usage/v9/breadcrumbs/collapsing-items/expand-on-click/index.md'; + + + +### Collapsed Indicator Click -- Present Popover + +The `ionCollapsedClick` event can also be used to present an overlay (in this case, an `ion-popover`) showing the hidden breadcrumbs. + +import PopoverOnClick from '@site/static/usage/v9/breadcrumbs/collapsing-items/popover-on-click/index.md'; + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/breadcrumbs/theming/colors/index.md'; + + + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/breadcrumbs/theming/css-properties/index.md'; + + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/button.md b/docs/api/button.md new file mode 100644 index 00000000000..cd960a1788d --- /dev/null +++ b/docs/api/button.md @@ -0,0 +1,115 @@ +--- +title: "ion-button" +--- +import Props from '@ionic-internal/component-api/v9/button/props.md'; +import Events from '@ionic-internal/component-api/v9/button/events.md'; +import Methods from '@ionic-internal/component-api/v9/button/methods.md'; +import Parts from '@ionic-internal/component-api/v9/button/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/button/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/button/slots.md'; + + + ion-button: Style Buttons with Custom CSS Properties + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Buttons provide a clickable element, which can be used in forms, or anywhere that needs simple, standard button functionality. They may display text, icons, or both. Buttons can be styled with several attributes to look a specific way. + +## Basic Usage + +import Basic from '@site/static/usage/v9/button/basic/index.md'; + + + +## Expand + +This property lets you specify how wide the button should be. By default, buttons have `display: inline-block`, but setting this property will change the button to a full-width element with `display: block`. + +import Expand from '@site/static/usage/v9/button/expand/index.md'; + + + +## Shape + +This property lets you specify the shape of the button. By default, buttons are rectangular with a small border radius, but setting this to `"round"` will change the button to a rounded element. + +import Shape from '@site/static/usage/v9/button/shape/index.md'; + + + + +## Fill + +This property determines the background and border color of the button. By default, buttons have a solid background unless the button is inside of a toolbar, in which case it has a transparent background. + +import Fill from '@site/static/usage/v9/button/fill/index.md'; + + + +## Size + +This property specifies the size of the button. Setting this property will change the height and padding of a button. + +import Size from '@site/static/usage/v9/button/size/index.md'; + + + +## Icons + +import Icons from '@site/static/usage/v9/button/icons/index.md'; + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/button/theming/colors/index.md'; + + + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/button/theming/css-properties/index.md'; + + + +## Accessibility + +Buttons are built to be accessible, but may need some adjustments depending on their content. The button component renders a native [button element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button) which allows it to take advantage of the functionality that a native button provides. + +### Overflowing Text Content + +There are many cases where a button's text content may overflow the container. It is recommended to wrap the text inside of the button when this happens so that all of the text can still be read. The button component will automatically adjust its height to accommodate the extra lines of text. + +The button text does not automatically wrap to the next line when the text is too long to fit. In order to make the text wrap, the `ion-text-wrap` class can be added, which will set the `white-space` property to `"normal"`. This will become the default in a future major release. + +:::info +The `max-width` style is set on the button below for demo purposes only. Text wrapping will work with a dynamic button width. +::: + +import TextWrapping from '@site/static/usage/v9/button/text-wrapping/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/buttons.md b/docs/api/buttons.md new file mode 100644 index 00000000000..847c13a6a99 --- /dev/null +++ b/docs/api/buttons.md @@ -0,0 +1,87 @@ +--- +title: "ion-buttons" +--- +import Props from '@ionic-internal/component-api/v9/buttons/props.md'; +import Events from '@ionic-internal/component-api/v9/buttons/events.md'; +import Methods from '@ionic-internal/component-api/v9/buttons/methods.md'; +import Parts from '@ionic-internal/component-api/v9/buttons/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/buttons/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/buttons/slots.md'; + + + ion-buttons: Toolbar Element with Named Slots for Buttons + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +The Buttons component is a container element. It should be used inside of a [toolbar](./toolbar) and can contain several types of buttons, including standard [buttons](./button), [menu buttons](./menu-button), and [back buttons](./back-button). + +## Basic Usage + +import Basic from '@site/static/usage/v9/buttons/basic/index.md'; + + + + +## Buttons Placement + +Buttons can be positioned inside of the toolbar using a named slot. The below chart has a description of each slot. + +| Slot | Description | +|--------------|----------------------------------------------------------------------------------------------------------| +| `start` | Positions to the `left` of the content in LTR, and to the `right` in RTL. | +| `end` | Positions to the `right` of the content in LTR, and to the `left` in RTL. | +| `secondary` | Positions element to the `left` of the content in `ios` mode, and directly to the `right` in `md` mode. | +| `primary` | Positions element to the `right` of the content in `ios` mode, and to the far `right` in `md` mode. | + +import Placement from '@site/static/usage/v9/buttons/placement/index.md'; + + + + +## Types of Buttons + +A button in a toolbar is styled to be clear by default, but this can be changed using the [`fill`](./button#fill) property on the button. The properties included on [back button](./back-button) and [menu button](./menu-button) in this example are for display purposes; refer to their respective documentation for proper usage. + +import Types from '@site/static/usage/v9/buttons/types/index.md'; + + + + +## Collapsible Buttons + +The `collapse` property can be set on the buttons to collapse them when the header collapses. This is typically used with [collapsible large titles](./title#collapsible-large-titles). + +:::info + +This feature is only available for iOS. + +::: + +{/* Reuse the playground from the Title directory */} +import CollapsibleLargeTitleButtons from '@site/static/usage/v9/title/collapsible-large-title/buttons/index.md'; + + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/card-content.md b/docs/api/card-content.md new file mode 100644 index 00000000000..439091a666d --- /dev/null +++ b/docs/api/card-content.md @@ -0,0 +1,35 @@ +--- +title: "ion-card-content" +--- +import Props from '@ionic-internal/component-api/v9/card-content/props.md'; +import Events from '@ionic-internal/component-api/v9/card-content/events.md'; +import Methods from '@ionic-internal/component-api/v9/card-content/methods.md'; +import Parts from '@ionic-internal/component-api/v9/card-content/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/card-content/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/card-content/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + +Card content is a child component of card that adds padding around its contents. It is recommended that any text content for a card should be placed inside of card content. + +Refer to the [Card](./card) documentation for more information. + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/card-header.md b/docs/api/card-header.md new file mode 100644 index 00000000000..fdd7abece06 --- /dev/null +++ b/docs/api/card-header.md @@ -0,0 +1,37 @@ +--- +title: "ion-card-header" +--- +import Props from '@ionic-internal/component-api/v9/card-header/props.md'; +import Events from '@ionic-internal/component-api/v9/card-header/events.md'; +import Methods from '@ionic-internal/component-api/v9/card-header/methods.md'; +import Parts from '@ionic-internal/component-api/v9/card-header/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/card-header/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/card-header/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +Card header is a child component of card that should be placed before the card content. It can contain a [card title](./card-title) and a [card subtitle](./card-subtitle). + +Refer to the [Card](./card) documentation for more information. + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/card-subtitle.md b/docs/api/card-subtitle.md new file mode 100644 index 00000000000..c15dd5d1403 --- /dev/null +++ b/docs/api/card-subtitle.md @@ -0,0 +1,37 @@ +--- +title: "ion-card-subtitle" +--- +import Props from '@ionic-internal/component-api/v9/card-subtitle/props.md'; +import Events from '@ionic-internal/component-api/v9/card-subtitle/events.md'; +import Methods from '@ionic-internal/component-api/v9/card-subtitle/methods.md'; +import Parts from '@ionic-internal/component-api/v9/card-subtitle/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/card-subtitle/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/card-subtitle/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +Card subtitle is a child component of card that should be placed inside of a [card header](./card-header). + +Refer to the [Card](./card) documentation for more information. + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/card-title.md b/docs/api/card-title.md new file mode 100644 index 00000000000..c2bfeca3a4f --- /dev/null +++ b/docs/api/card-title.md @@ -0,0 +1,42 @@ +--- +title: "ion-card-title" +--- +import Props from '@ionic-internal/component-api/v9/card-title/props.md'; +import Events from '@ionic-internal/component-api/v9/card-title/events.md'; +import Methods from '@ionic-internal/component-api/v9/card-title/methods.md'; +import Parts from '@ionic-internal/component-api/v9/card-title/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/card-title/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/card-title/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + ion-card-title: Ionic App Card Title Component + + + + + + +Card title is a child component of card that should be placed inside of a [card header](./card-header). + +Refer to the [Card](./card) documentation for more information. + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/card.md b/docs/api/card.md new file mode 100644 index 00000000000..0d1c1785f93 --- /dev/null +++ b/docs/api/card.md @@ -0,0 +1,87 @@ +--- +title: "ion-card" +--- +import Props from '@ionic-internal/component-api/v9/card/props.md'; +import Events from '@ionic-internal/component-api/v9/card/events.md'; +import Methods from '@ionic-internal/component-api/v9/card/methods.md'; +import Parts from '@ionic-internal/component-api/v9/card/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/card/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/card/slots.md'; + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + ion-card: Card UI Components for Ionic Framework API + + + + + + +Cards are containers that display content such as text, images, buttons, and lists. +A card can be a single component, but is often made up of a header, title, subtitle, +and content. Cards are broken up into several components to accommodate this structure: +[card header](./card-header), [card title](./card-title), [card subtitle](./card-subtitle), +and [card content](./card-content). + + +## Basic Usage + +import Basic from '@site/static/usage/v9/card/basic/index.md'; + + + + +## Media Cards + +import Media from '@site/static/usage/v9/card/media/index.md'; + + + + +## Card Buttons + +import Buttons from '@site/static/usage/v9/card/buttons/index.md'; + + + + +## List Card + +import List from '@site/static/usage/v9/card/list/index.md'; + + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/card/theming/colors/index.md'; + + + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/card/theming/css-properties/index.md'; + + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/checkbox.md b/docs/api/checkbox.md new file mode 100644 index 00000000000..5b712b5ac9b --- /dev/null +++ b/docs/api/checkbox.md @@ -0,0 +1,133 @@ +--- +title: "ion-checkbox" +--- + +import Props from '@ionic-internal/component-api/v9/checkbox/props.md'; +import Events from '@ionic-internal/component-api/v9/checkbox/events.md'; +import Methods from '@ionic-internal/component-api/v9/checkbox/methods.md'; +import Parts from '@ionic-internal/component-api/v9/checkbox/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/checkbox/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/checkbox/slots.md'; + + + ion-checkbox: Ionic App Checkbox to Select Multiple Options + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +Checkboxes allow the selection of multiple options from a set of options. They appear as checked (ticked) when activated. Clicking on a checkbox will toggle the `checked` property. They can also be checked programmatically by setting the `checked` property. + +## Basic Usage + +import Basic from '@site/static/usage/v9/checkbox/basic/index.md'; + + + +## Label Placement + +Developers can use the `labelPlacement` property to control how the label is placed relative to the control. This property mirrors the flexbox `flex-direction` property. + +import LabelPlacement from '@site/static/usage/v9/checkbox/label-placement/index.md'; + + + +## Alignment + +Developers can use the `alignment` property to control how the label and control are aligned on the cross axis. This property mirrors the flexbox `align-items` property. + +:::note +Stacked checkboxes can be aligned using the `alignment` property. This can be useful when the label and control need to be centered horizontally. +::: + +import Alignment from '@site/static/usage/v9/checkbox/alignment/index.md'; + + + +## Justification + +Developers can use the `justify` property to control how the label and control are packed on a line. This property mirrors the flexbox `justify-content` property. + +import Justify from '@site/static/usage/v9/checkbox/justify/index.md'; + + + + +:::note +`ion-item` is only used in the demos to emphasize how `justify` works. It is not needed in order for `justify` to function correctly. +::: + +## Indeterminate Checkboxes + +import Indeterminate from '@site/static/usage/v9/checkbox/indeterminate/index.md'; + + + +## Links inside of Labels + +Checkbox labels can sometimes be accompanied with links. These links can provide more information related to the checkbox. However, clicking the link should not check the checkbox. To achieve this, we can use [stopPropagation](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation) to prevent the click event from bubbling. When using this approach, the rest of the label still remains clickable. + +import LabelLink from '@site/static/usage/v9/checkbox/label-link/index.md'; + + + +## Helper & Error Text + +Helper and error text can be used inside of a checkbox with the `helperText` and `errorText` property. The error text will not be displayed unless the `ion-invalid` and `ion-touched` classes are added to the `ion-checkbox`. This ensures errors are not shown before the user has a chance to enter data. + +In Angular, this is done automatically through form validation. In JavaScript, React and Vue, the class needs to be manually added based on your own validation. + +import HelperError from '@site/static/usage/v9/checkbox/helper-error/index.md'; + + + +## Theming + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/checkbox/theming/css-properties/index.md'; + + + +## Interfaces + +### CheckboxChangeEventDetail + +```typescript +interface CheckboxChangeEventDetail { + value: T; + checked: boolean; +} +``` + +### CheckboxCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing with Ionic events emitted from this component. + +```typescript +interface CheckboxCustomEvent extends CustomEvent { + detail: CheckboxChangeEventDetail; + target: HTMLIonCheckboxElement; +} +``` + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/chip.md b/docs/api/chip.md new file mode 100644 index 00000000000..88911a49893 --- /dev/null +++ b/docs/api/chip.md @@ -0,0 +1,64 @@ +--- +title: "ion-chip" +--- +import Props from '@ionic-internal/component-api/v9/chip/props.md'; +import Events from '@ionic-internal/component-api/v9/chip/events.md'; +import Methods from '@ionic-internal/component-api/v9/chip/methods.md'; +import Parts from '@ionic-internal/component-api/v9/chip/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/chip/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/chip/slots.md'; + + + ion-chip: Text, Icon and Avatar for Ionic Framework Apps + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Chips represent complex entities in small blocks, such as a contact. A chip can contain several different elements such as avatars, text, and icons. + +## Basic Usage + +import Basic from '@site/static/usage/v9/chip/basic/index.md'; + + + +## Slotting Components and Icons + +import SlotExample from '@site/static/usage/v9/chip/slots/index.md'; + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/chip/theming/colors/index.md'; + + + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/chip/theming/css-properties/index.md'; + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/col.md b/docs/api/col.md new file mode 100644 index 00000000000..1d41cc82580 --- /dev/null +++ b/docs/api/col.md @@ -0,0 +1,49 @@ +--- +title: "ion-col" +--- +import Props from '@ionic-internal/component-api/v9/col/props.md'; +import Events from '@ionic-internal/component-api/v9/col/events.md'; +import Methods from '@ionic-internal/component-api/v9/col/methods.md'; +import Parts from '@ionic-internal/component-api/v9/col/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/col/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/col/slots.md'; + + + ion-col: Column Component Padding and Other Properties + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +Columns are cellular components of the [grid](./grid) system and go inside of a [row](./row). They will expand to fill the row. All content within a grid should go inside of a column. + +Refer to the [grid](./grid) documentation for more information. + + +## Column Alignment + +By default, columns will stretch to fill the entire height of the row. Columns are [flex items](https://developer.mozilla.org/en-US/docs/Glossary/Flex_Item), so there are several [CSS classes](/docs/layout/css-utilities#flex-item-properties) that can be applied to a column to customize this behavior. + + + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/content.md b/docs/api/content.md new file mode 100644 index 00000000000..43ad7e4bc61 --- /dev/null +++ b/docs/api/content.md @@ -0,0 +1,181 @@ +--- +title: "ion-content" +--- +import Props from '@ionic-internal/component-api/v9/content/props.md'; +import Events from '@ionic-internal/component-api/v9/content/events.md'; +import Methods from '@ionic-internal/component-api/v9/content/methods.md'; +import Parts from '@ionic-internal/component-api/v9/content/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/content/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/content/slots.md'; + + + ion-content: Scrollable Component for Ionic App Content + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + + +The content component provides an easy to use content area with some useful methods +to control the scrollable area. There should only be one content in a single +view. + +Content, along with many other Ionic components, can be customized to modify its padding, margin, and more using the global styles provided in the [CSS Utilities](/docs/layout/css-utilities) or by individually styling it using CSS and the available [CSS Custom Properties](#css-custom-properties). + + +## Basic Usage + +import Basic from '@site/static/usage/v9/content/basic/index.md'; + + + + +## Header & Footer + +Content can be the only top-level component in a page, or it can be used alongside a [header](./header), [footer](./footer), or both. When used with a header or footer, it will adjust its size to fill the remaining height. + +import HeaderFooter from '@site/static/usage/v9/content/header-footer/index.md'; + + + + +## Fullscreen Content + +By default, content fills the space between a [header](./header) and [footer](./footer) but does not go behind them. In certain cases, it may be desired to have the content scroll behind the header and footer, such as when the `translucent` property is set on either of them, or `opacity` is set on the toolbar. This can be achieved by setting the `fullscreen` property on the content to `true`. + +import Fullscreen from '@site/static/usage/v9/content/fullscreen/index.md'; + + + + +## Fixed Content + +To place elements outside of the scrollable area, assign them to the `fixed` slot. Doing so will [absolutely position](https://developer.mozilla.org/en-US/docs/Web/CSS/position#absolute_positioning) the element to the top left of the content. In order to change the position of the element, it can be styled using the [top, right, bottom, and left](https://developer.mozilla.org/en-US/docs/Web/CSS/position) CSS properties. + +The `fixedSlotPlacement` property is used to determine if content in the `fixed` slot is placed before or after the main content in the DOM. When set to `before`, fixed slot content will be placed before the main content and will therefore receive keyboard focus before the main content receives keyboard focus. This can be useful when the main content contains an infinitely-scrolling list, preventing a [FAB](./fab) or other fixed content from being reachable by pressing the tab key. + +import Fixed from '@site/static/usage/v9/content/fixed/index.md'; + + + +## Scroll Methods + +Content provides [methods](#methods) that can be called to scroll the content to the bottom, top, or to a specific point. They can be passed a `duration` in order to smoothly transition instead of instantly changing the position. + +import ScrollMethods from '@site/static/usage/v9/content/scroll-methods/index.md'; + + + +## Scroll Events + +Scroll events are disabled by default for content due to performance. However, they can be enabled by setting `scrollEvents` to `true`. This is necessary before listening to any of the scroll [events](#events). + +import ScrollEvents from '@site/static/usage/v9/content/scroll-events/index.md'; + + + + +## Theming + +### Colors + +import Colors from '@site/static/usage/v9/content/theming/colors/index.md'; + + + +### CSS Shadow Parts + +import CSSParts from '@site/static/usage/v9/content/theming/css-shadow-parts/index.md'; + + + +### CSS Custom Properties + +import CSSProps from '@site/static/usage/v9/content/theming/css-properties/index.md'; + + + +### Safe Area Padding + +The content component will not automatically apply padding to any of its sides to account for the [safe area](/docs/theming/advanced#safe-area-padding). This is because the content component is often used in conjunction with other components that apply their own padding, such as [headers](./header) and [footers](./footer). However, if the content component is being used on its own, it may be desired to apply padding to the safe area. This can be done through CSS by using the `--ion-safe-area-(dir)` variables described in [Application Variables](../theming/advanced.md#application-variables). + +The most common use case for this is to apply padding to the top of the content to account for the status bar. This can be done by setting the `padding-top` property to the value of the `--ion-safe-area-top` variable. + +```css +ion-content::part(scroll) { + padding-top: var(--ion-safe-area-top, 0); +} +``` + +Another common use case is to apply padding to the left side of the content to account for the notch when the device is in landscape mode and the notch is on the left side. This can be done by setting the `padding-left` property to the value of the `--ion-safe-area-left` variable. + +```css +ion-content::part(scroll) { + padding-left: var(--ion-safe-area-left, 0); +} +``` + +import SafeArea from '@site/static/usage/v9/content/theming/safe-area/index.md'; + + + +## Interfaces + +### ScrollBaseDetail + +```typescript +interface ScrollBaseDetail { + isScrolling: boolean; +} +``` + +### ScrollDetail + +```typescript +interface ScrollDetail extends GestureDetail, ScrollBaseDetail { + scrollTop: number; + scrollLeft: number; +} +``` + +### ScrollBaseCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing on the `ionScrollStart` and `ionScrollEnd` events. + +```typescript +interface ScrollBaseCustomEvent extends CustomEvent { + detail: ScrollBaseDetail; + target: HTMLIonContentElement; +} +``` + +### ScrollCustomEvent + +While not required, this interface can be used in place of the `CustomEvent` interface for stronger typing on the `ionScroll` event. + +```typescript +interface ScrollCustomEvent extends ScrollBaseCustomEvent { + detail: ScrollDetail; +} +``` + + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/datetime-button.md b/docs/api/datetime-button.md new file mode 100644 index 00000000000..47ac470f16d --- /dev/null +++ b/docs/api/datetime-button.md @@ -0,0 +1,66 @@ +--- +title: "ion-datetime-button" +--- +import Props from '@ionic-internal/component-api/v9/datetime-button/props.md'; +import Events from '@ionic-internal/component-api/v9/datetime-button/events.md'; +import Methods from '@ionic-internal/component-api/v9/datetime-button/methods.md'; +import Parts from '@ionic-internal/component-api/v9/datetime-button/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/datetime-button/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/datetime-button/slots.md'; + + + ion-datetime-button: Ionic Input for Datetime Picker + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Datetime Button links with a [Datetime](./datetime) component to display the formatted date and time. It also provides buttons to present the datetime in a modal, popover, and more. + +## Overview + +Datetime Button should be used when space is constrained. This component displays buttons which show the current date and time values. When the buttons are tapped, the date or time pickers open in the overlay. + +When using Datetime Button with a JavaScript framework such as Angular, React, or Vue be sure to use the [keepContentsMounted property on ion-modal](./modal#prop-keep-contents-mounted) or the [keepContentsMounted property on ion-popover](./popover#prop-keep-contents-mounted). This allows the linked datetime instance to be mounted even if the overlay has not been presented yet. + +## Basic Usage + +import Basic from '@site/static/usage/v9/datetime-button/basic/index.md'; + + + +## Localization + +The localized text on `ion-datetime-button` is determined by the `locale` property on the associated `ion-datetime` instance. Refer to [Datetime Localization](./datetime#localization) for more details. + +## Format Options + +You can customize the format of the date and time in a Datetime Button by providing `formatOptions` on the associated Datetime instance. Refer to [Datetime Format Options](./datetime#format-options) for more details. + +import FormatOptions from '@site/static/usage/v9/datetime-button/format-options/index.md'; + + + +## Usage with Modals and Popovers + +`ion-datetime-button` must be associated with a mounted `ion-datetime` instance. As a result, [Inline Modals](./modal#inline-modals-recommended) and [Inline Popovers](./popover#inline-popovers) with the `keepContentsMounted` property set to `true` must be used. + +## Properties + + +## Events + + +## Methods + + +## CSS Shadow Parts + + +## CSS Custom Properties + + +## Slots + diff --git a/docs/api/datetime.md b/docs/api/datetime.md new file mode 100644 index 00000000000..00c770971e9 --- /dev/null +++ b/docs/api/datetime.md @@ -0,0 +1,519 @@ +--- +title: "ion-datetime" +--- +import Props from '@ionic-internal/component-api/v9/datetime/props.md'; +import Events from '@ionic-internal/component-api/v9/datetime/events.md'; +import Methods from '@ionic-internal/component-api/v9/datetime/methods.md'; +import Parts from '@ionic-internal/component-api/v9/datetime/parts.md'; +import CustomProps from '@ionic-internal/component-api/v9/datetime/custom-props.mdx'; +import Slots from '@ionic-internal/component-api/v9/datetime/slots.md'; + +import Basic from '@site/static/usage/v9/datetime/basic/index.md'; + +import MaxMin from '@site/static/usage/v9/datetime/date-constraints/max-min/index.md'; +import Values from '@site/static/usage/v9/datetime/date-constraints/values/index.md'; +import Advanced from '@site/static/usage/v9/datetime/date-constraints/advanced/index.md'; + +import FormatOptions from '@site/static/usage/v9/datetime/format-options/index.md'; + +import CustomLocale from '@site/static/usage/v9/datetime/localization/custom-locale/index.md'; +import HourCycle from '@site/static/usage/v9/datetime/localization/hour-cycle/index.md'; +import FirstDayOfWeek from '@site/static/usage/v9/datetime/localization/first-day-of-week/index.md'; +import LocaleExtensionTags from '@site/static/usage/v9/datetime/localization/locale-extension-tags/index.md'; +import TimeLabel from '@site/static/usage/v9/datetime/localization/time-label/index.md'; + +import MonthAndYear from '@site/static/usage/v9/datetime/presentation/month-and-year/index.md'; +import Time from '@site/static/usage/v9/datetime/presentation/time/index.md'; +import Date from '@site/static/usage/v9/datetime/presentation/date/index.md'; + +import ShowingDefaultTitle from '@site/static/usage/v9/datetime/title/showing-default-title/index.md'; +import CustomizingTitle from '@site/static/usage/v9/datetime/title/customizing-title/index.md'; + +import ShowingConfirmationButtons from '@site/static/usage/v9/datetime/buttons/showing-confirmation-buttons/index.md'; +import CustomizingButtons from '@site/static/usage/v9/datetime/buttons/customizing-buttons/index.md'; +import CustomizingButtonTexts from '@site/static/usage/v9/datetime/buttons/customizing-button-texts/index.md'; + +import HighlightedDatesArray from '@site/static/usage/v9/datetime/highlightedDates/array/index.md'; +import HighlightedDatesCallback from '@site/static/usage/v9/datetime/highlightedDates/callback/index.md'; + +import ShowAdjacentDays from '@site/static/usage/v9/datetime/show-adjacent-days/index.md'; + +import MultipleDateSelection from '@site/static/usage/v9/datetime/multiple/index.md'; + +import GlobalTheming from '@site/static/usage/v9/datetime/styling/global-theming/index.md'; +import CalendarHeaderStyling from '@site/static/usage/v9/datetime/styling/calendar-header/index.md'; +import CalendarDaysStyling from '@site/static/usage/v9/datetime/styling/calendar-days/index.md'; +import DatetimeHeaderStyling from '@site/static/usage/v9/datetime/styling/datetime-header/index.md'; +import WheelStyling from '@site/static/usage/v9/datetime/styling/wheel-styling/index.md'; + + + ion-datetime: Ionic API Input for Datetime Format Picker + + + +import EncapsulationPill from '@components/page/api/EncapsulationPill'; + + + +Datetimes present a calendar interface and time wheel, making it easy for users to select dates and times. Datetimes are similar to the native `input` elements of `datetime-local`, however, Ionic Framework's Datetime component makes it easy to display the date and time in the preferred format, and manage the datetime values. + +## Overview + +Historically, handling datetime values within JavaScript, or even within HTML +inputs, has always been a challenge. Specifically, JavaScript's `Date` object is +notoriously difficult to correctly parse apart datetime strings or to format +datetime values. Even worse is how different browsers and JavaScript versions +parse various datetime strings differently, especially per locale. + +Fortunately, Ionic Framework's datetime input has been designed so developers can avoid +the common pitfalls, allowing developers to easily manipulate datetime values and give the user a simple datetime picker for a great user experience. + +### ISO 8601 Datetime Format: `YYYY-MM-DDTHH:mmZ` + +Ionic Framework uses the [ISO 8601 datetime format](https://www.w3.org/TR/NOTE-datetime) +for its value. The value is simply a string, rather than using JavaScript's +`Date` object. Using the ISO datetime format makes it easy to serialize +and parse within JSON objects and databases. + +Below are some examples of ISO 8601 formats that can be used with `ion-datetime`: + +| Description | Format | Datetime Value Example | +| -------------------- | ------------------------ | ------------------------------ | +| Year | `YYYY` | `1994` | +| Year and Month | `YYYY-MM` | `1994-12` | +| Complete Date | `YYYY-MM-DD` | `1994-12-15` | +| Date and Time | `YYYY-MM-DDTHH:mm` | `1994-12-15T13:47` | +| UTC Timezone | `YYYY-MM-DDTHH:mm:ssZ` | `1994-12-15T13:47:20Z` | +| Timezone Offset | `YYYY-MM-DDTHH:mm:ssTZD` | `1994-12-15T13:47:20+05:00` | +| Hour and Minute | `HH:mm` | `13:47` | + +Note that the year is always four-digits, milliseconds (if it's added) is always +three-digits, and all others are always two-digits. So the number representing +January always has a leading zero, such as `01`. Additionally, the hour is +always in the 24-hour format, so `00` is `12am` on a 12-hour clock, `13` means +`1pm`, and `23` means `11pm`. + +:::note +While seconds, milliseconds, and time zone can be specified using the ISO 8601 datetime format, `ion-datetime` does not provide an interface for second, millisecond, and time zone selection. Any second, millisecond, or time zone values provided will be ignored. +::: + +## Basic Usage + + + +## Usage with Datetime Button + +If you need to present a datetime in an overlay such as a modal or a popover, we recommend using [ion-datetime-button](./datetime-button). `ion-datetime-button` should be used when space is constrained. This component displays buttons which show the current date and time values. When the buttons are tapped, the date or time pickers open in the overlay. + +## Setting Values Asynchronously + +If its `value` is updated programmatically after a datetime has already been created, the datetime will automatically jump to the new date. However, it is recommended to avoid updating the `value` in this way when users are able to interact with the datetime, as this could be disorienting for those currently trying to select a date. For example, if a datetime's `value` is loaded by an asynchronous process, it is recommended to hide the datetime with CSS until the value has finished updating. + +## Date Constraints + +### Max and Min Dates + +To customize the minimum and maximum datetime values, the `min` and `max` component properties can be provided which may make more sense for the app's use-case. Following the same IS0 8601 format listed in the table above, each component can restrict which dates can be selected by the user. + +The following example restricts date selection to March 2022 through May 2022 only. + + + +### Selecting Specific Values + +While the `min` and `max` properties allow you to restrict date selection to a certain range, the `monthValues`, `dayValues`, `yearValues`, `hourValues`, and `minuteValues` properties allow you choose specific days and times that users can select. + +The following example allows minutes to be selected in increments of 15. It also allows for days to be selected in increments of 5. + + + +### Advanced Date Constraints + +With the `isDateEnabled` property, developers can customize the `ion-datetime` to disable a specific day, range of dates, weekends or any custom rule using an ISO 8601 date string. +The `isDateEnabled` property accepts a function returning a boolean, indicating if a date is enabled. The function is called for each rendered calendar day, for the previous, current and next month. Custom implementations should be optimized for performance to avoid jank. + +The following example shows how to disable all weekend dates. For more advanced date manipulation, we recommend using a date utility such as `date-fns`. + + + +## Localization + +Ionic Framework makes use of the [Intl.DatetimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DatetimeFormat) Web API which allows us to automatically localize the month and day names according to the language and region set on the user's device. + +### Custom Locale + +For instances where you need a specific locale, you can use the `locale` property to set it. The locale controls both the language and the date and time formats that are displayed. + +The following example shows how to set the locale to Spanish (Spain). + + + +:::note +The time label is not automatically localized. Refer to [Time Label](#time-label) for more information. +::: + +### Hour Cycle + +`ion-datetime` will use the hour cycle that is specified by the `locale` property by default. For example, if `locale` is set to `en-US`, then `ion-datetime` will use a 12 hour cycle. + +There are 4 primary hour cycle types: + +| Hour cycle type | Description | +| --------------- | ------------------------------------------------------------ | +| `'h12'` | Hour system using 1–12; corresponds to 'h' in patterns. The 12 hour clock, with midnight starting at 12:00 am. | +| `'h23'` | Hour system using 0–23; corresponds to 'H' in patterns. The 24 hour clock, with midnight starting at 0:00. | +| `'h11'` | Hour system using 0–11; corresponds to 'K' in patterns. The 12 hour clock, with midnight starting at 0:00 am. | +| `'h24'` | Hour system using 1–24; corresponds to 'k' in pattern. The 24 hour clock, with midnight starting at 24:00. | + +:::note + Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle +::: + + +There may be scenarios where you need to have more control over which hour cycle is used. This is where the `hourCycle` property can help. + +In the following example, we can use the `hourCycle` property to force `ion-datetime` to use the 12 hour cycle even though the locale is `en-GB`, which uses a 24 hour cycle by default: + + + +### First Day of the Week + +For `ion-datetime`, the default first day of the week is Sunday. As of 2022, there is no browser API that lets Ionic automatically determine the first day of the week based on a device's locale, though there is on-going work regarding this (refer to [TC39 GitHub](https://github.com/tc39/ecma402/issues/6)). + + + +### Time Label + +The time label is not automatically localized. Fortunately, Ionic makes it easy to provide custom localizations with the `time-label` slot. + + + +### Locale Extension Tags + +`ion-datetime` also supports [locale extension tags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) as part of the `Intl.Locale` API. These tags let you encode information about the locale in the locale string itself. Developers may prefer to use the extension tag approach if they are using the [Intl.Locale API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) in their apps. + +For example, if you wanted to use a 12 hour cycle with the `en-GB` locale, you could provide extension tags instead of using both the `locale` and `hourCycle` properties: + + + +:::note +Be sure to check the [Browser Compatibility Chart](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale#browser_compatibility) for `Intl.Locale` before using it in your app. +::: + +## Presentation + +By default, `ion-datetime` allows users to select both date and time. In addition, users have access to selecting the specific month, year, hour, and minute. + +Some use cases may call for only date selection or only time selection. The `presentation` property allows you to specify which pickers to show and the order to show them in. For example, setting `date-time` will have the calendar picker appear before the time picker. Setting `time-date` will have the calendar picker appear after the time picker. + +### Month and Year Selection + +Month and year selection is available by passing `month-year`, `month`, or `year` to the `presentation` property. + +This example shows a datetime with the `month-year` configuration. + + + +### Time Selection + +Time selection is available by passing `date-time`, `time-date`, or `time` to the `presentation` property. + +This example shows a datetime with the `time` configuration. + +