Initial commit
This commit is contained in:
Коммит
2f05f285f6
|
@ -0,0 +1,87 @@
|
|||
version: 2
|
||||
|
||||
# Defining default values for all jobs
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: circleci/node:lts-browsers
|
||||
|
||||
jobs:
|
||||
setup_tooling:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
# - Install npm dependencies for LWC linting and testing
|
||||
- run:
|
||||
name: Install package dependencies
|
||||
command: |
|
||||
yarn install
|
||||
- persist_to_workspace:
|
||||
# This is an important step. If we don't store the project data (cloned GitHub source and node_modules)
|
||||
# we'd have to re-run installation for every workflow step.
|
||||
root: ~/
|
||||
paths:
|
||||
- project/*
|
||||
prettier_verify:
|
||||
# This verifies Prettier formatting
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Verify Prettier formatting
|
||||
command: |
|
||||
yarn prettier:verify
|
||||
lint_lwc:
|
||||
# This lints Lightning Web Components.
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Lint Lightning Web Components
|
||||
command: |
|
||||
yarn lint:lwc
|
||||
unit_test_lwc:
|
||||
# This unit tests Lightning Web Components.
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Unit test Lightning Web Components
|
||||
command: |
|
||||
npm run test:unit:coverage -- -- --runInBand
|
||||
- persist_to_workspace:
|
||||
# We're saving the generated coverage results (folder 'coverage', a sub-folder to 'project') so
|
||||
# that we can upload them in another step to Codecov.io.
|
||||
root: ~/
|
||||
paths:
|
||||
- project/*
|
||||
upload_code_coverage:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Push to Codecov.io
|
||||
command: |
|
||||
# Uploading Apex and LWC tests
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- setup_tooling
|
||||
- prettier_verify:
|
||||
requires:
|
||||
- setup_tooling
|
||||
- lint_lwc:
|
||||
requires:
|
||||
- prettier_verify
|
||||
- unit_test_lwc:
|
||||
requires:
|
||||
- lint_lwc
|
||||
- upload_code_coverage:
|
||||
requires:
|
||||
- unit_test_lwc
|
|
@ -0,0 +1 @@
|
|||
src/resources/external/*
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": ["@salesforce/eslint-config-lwc/recommended"],
|
||||
"rules": {
|
||||
"@lwc/lwc/no-async-operation": "warn",
|
||||
"@lwc/lwc/no-inner-html": "warn",
|
||||
"@lwc/lwc/no-document-query": "warn"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve this sample application.
|
||||
---
|
||||
|
||||
<!--
|
||||
NOTICE: Please use the bug report only for reporting bugs on the application itself.
|
||||
|
||||
For general bugs with Lightning Web Components Open Source visit the LWC repository on https://github.com/salesforce/lwc.
|
||||
|
||||
If you experience issues with `lwc-create-app` or `lwc-services` please visit https://github.com/muenzpraeger/lwc-create-app.
|
||||
-->
|
||||
|
||||
### Summary
|
||||
|
||||
_Short summary of what is going on or to provide context_.
|
||||
|
||||
### Steps To Reproduce:
|
||||
|
||||
1. This is step 1.
|
||||
1. This is step 2. All steps should start with '1.'
|
||||
|
||||
### Expected result
|
||||
|
||||
_Describe what should have happened_.
|
||||
|
||||
### Actual result
|
||||
|
||||
_Describe what actually happened instead_.
|
||||
|
||||
### Additional information
|
||||
|
||||
_Feel free to attach a screenshot or code snippets_.
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
---
|
||||
|
||||
<!--
|
||||
NOTICE: Please use the bug report only for reporting bugs on the application itself.
|
||||
|
||||
For general feature requests about Lightning Web Components Open Source visit the LWC repository on https://github.com/salesforce/lwc.
|
||||
|
||||
For feature requests about `lwc-create-app` or `lwc-services` please visit https://github.com/muenzpraeger/lwc-create-app.
|
||||
-->
|
||||
|
||||
**What is missing from the application? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'd like to see an implementation of [...]
|
||||
|
||||
**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.
|
|
@ -0,0 +1,3 @@
|
|||
**Looking for help?**
|
||||
|
||||
Check out the [Lightning Web Components Open Source documentation](https://lwc.dev).
|
|
@ -0,0 +1,27 @@
|
|||
# Log files
|
||||
logs
|
||||
*.log
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
# Standard dist folder
|
||||
/dist
|
||||
|
||||
# Tooling files
|
||||
node_modules
|
||||
jsconfig.json
|
||||
|
||||
# Temp directory
|
||||
/tmp
|
||||
|
||||
# Jest coverage folder
|
||||
/coverage
|
||||
|
||||
# MacOS system files
|
||||
.DS_Store
|
||||
|
||||
# Windows system files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
[Dd]esktop.ini
|
||||
$RECYCLE.BIN/
|
|
@ -0,0 +1,3 @@
|
|||
coverage/
|
||||
dist/
|
||||
src/resources
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "**/*.html",
|
||||
"options": { "parser": "lwc" }
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
# Salesforce Open Source Community Code of Conduct
|
||||
|
||||
## About the Code of Conduct
|
||||
|
||||
Equality is a core value at Salesforce. We believe a diverse and inclusive
|
||||
community fosters innovation and creativity, and are committed to building a
|
||||
culture where everyone feels included.
|
||||
|
||||
Salesforce open-source projects are committed to providing a friendly, safe, and
|
||||
welcoming environment for all, regardless of gender identity and expression,
|
||||
sexual orientation, disability, physical appearance, body size, ethnicity, nationality,
|
||||
race, age, religion, level of experience, education, socioeconomic status, or
|
||||
other similar personal characteristics.
|
||||
|
||||
The goal of this code of conduct is to specify a baseline standard of behavior so
|
||||
that people with different social values and communication styles can work
|
||||
together effectively, productively, and respectfully in our open source community.
|
||||
It also establishes a mechanism for reporting issues and resolving conflicts.
|
||||
|
||||
All questions and reports of abusive, harassing, or otherwise unacceptable behavior
|
||||
in a Salesforce open-source project may be reported by contacting the Salesforce
|
||||
Open Source Conduct Committee at ossconduct@salesforce.com.
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of gender
|
||||
identity and expression, sexual orientation, disability, physical appearance,
|
||||
body size, ethnicity, nationality, race, age, religion, level of experience, education,
|
||||
socioeconomic status, or other similar personal characteristics.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy toward other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Personal attacks, insulting/derogatory comments, or trolling
|
||||
- Public or private harassment
|
||||
- Publishing, or threatening to publish, others' private information—such as
|
||||
a physical or electronic address—without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- Advocating for or encouraging any of the above behaviors
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned with this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project email
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the Salesforce Open Source Conduct Committee
|
||||
at ossconduct@salesforce.com. All complaints will be reviewed and investigated
|
||||
and will result in a response that is deemed necessary and appropriate to the
|
||||
circumstances. The committee is obligated to maintain confidentiality with
|
||||
regard to the reporter of an incident. Further details of specific enforcement
|
||||
policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership and the Salesforce Open Source Conduct
|
||||
Committee.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home],
|
||||
version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.
|
||||
It includes adaptions and additions from [Go Community Code of Conduct][golang-coc],
|
||||
[CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc].
|
||||
|
||||
This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us].
|
||||
|
||||
[contributor-covenant-home]: https://www.contributor-covenant.org 'https://www.contributor-covenant.org/'
|
||||
[golang-coc]: https://golang.org/conduct
|
||||
[cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md
|
||||
[microsoft-coc]: https://opensource.microsoft.com/codeofconduct/
|
||||
[cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/
|
|
@ -0,0 +1,46 @@
|
|||
## Contributing
|
||||
|
||||
1. Familiarize yourself with the codebase.
|
||||
1. Create a new issue before starting your project so that we can keep track of
|
||||
what you are trying to add/fix. That way, we can also offer suggestions or
|
||||
let you know if there is already an effort in progress. We will let you know when you're good to go to start.
|
||||
1. Fork this repository.
|
||||
1. The [README](README.md) has details on how to set up your environment.
|
||||
1. Create a _topic_ branch in your fork based on the correct branch (usually the **master** branch. Note, this step is recommended but technically not required if contributing using a fork.
|
||||
1. Edit the code in your fork.
|
||||
1. Sign CLA (see [CLA](#cla) below)
|
||||
1. Send us a pull request when you are done. We'll review your code, suggest any
|
||||
needed changes, and merge it in.
|
||||
|
||||
### CLA
|
||||
|
||||
External contributors will be required to sign a Contributor's License
|
||||
Agreement. You can do so by going to https://cla.salesforce.com/sign-cla.
|
||||
|
||||
## Branches
|
||||
|
||||
- We work in `master`.
|
||||
- Our released (aka. _production_) branch is `master`.
|
||||
- Our work happens in _topic_ branches (feature and/or bug-fix).
|
||||
- feature as well as bug-fix branches are based on `master`
|
||||
- branches _should_ be kept up-to-date using `rebase`
|
||||
- see below for further merge instructions
|
||||
|
||||
### Merging between branches
|
||||
|
||||
- We try to limit merge commits as much as possible.
|
||||
|
||||
- _Topic_ branches are:
|
||||
|
||||
1. based on `master` and will be
|
||||
1. squash-merged into `master`.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- Develop features and bug fixes in _topic_ branches.
|
||||
- _Topic_ branches can live in forks (external contributors) or within this repository (committers).
|
||||
\*\* When creating _topic_ branches in this repository please prefix with `<developer-name>/`.
|
||||
|
||||
### Merging Pull Requests
|
||||
|
||||
- Pull request merging is restricted to squash & merge only.
|
|
@ -0,0 +1,119 @@
|
|||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
|
@ -0,0 +1 @@
|
|||
web: yarn serve:heroku
|
|
@ -0,0 +1,26 @@
|
|||
# Lightning Web Components Recipes Open Source
|
||||
|
||||
A collection of easy-to-digest code examples for Lightning Web Components Open Source. Each recipe demonstrates how to code a specific task in 30 lines of code or less. A View Source link takes you right to the code in GitHub. From Hello World to data access and third-party libraries, there is a recipe for that!
|
||||
|
||||
## Local Development
|
||||
|
||||
1. Clone the `lwc-recipes-oss` repository:
|
||||
|
||||
```
|
||||
git clone https://github.com/trailheadapps/lwc-recipes-oss
|
||||
cd lwc-recipes-oss
|
||||
```
|
||||
|
||||
2. Install the project dependencies using `yarn` (or `npm`, if you prefer that alternatively)
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. Start the app in watch mode.
|
||||
|
||||
```
|
||||
yarn watch
|
||||
```
|
||||
|
||||
4. Enjoy the app!
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
resources: [{ from: 'src/resources', to: 'dist/resources' }]
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "lwc-recipes-oss",
|
||||
"version": "0.1.0",
|
||||
"author": "Salesforce Developer Evangelism",
|
||||
"bugs": "https://github.com/trailheadapps/lwc-recipes-oss/issues",
|
||||
"dependencies": {
|
||||
"chart.js": "^2.8.0",
|
||||
"d3": "^5.9.2",
|
||||
"lwc-services": "^1",
|
||||
"moment": "^2.24.0"
|
||||
},
|
||||
"description": "Lightning Web Components Recipes Open Source",
|
||||
"devDependencies": {
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.5",
|
||||
"prettier": "^1.17"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/trailheadapps/lwc-recipes-oss",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-push": "lint-staged"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"lwc"
|
||||
],
|
||||
"license": "CC0-1.0",
|
||||
"lint-staged": {
|
||||
"**/*.{html,js,json,yaml,yml,md}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"**/modules/**": [
|
||||
"eslint"
|
||||
],
|
||||
"*": [
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"repository": "trailheadapps/lwc-recipes-oss",
|
||||
"scripts": {
|
||||
"build": "lwc-services build",
|
||||
"build:production": "lwc-services build --mode=production",
|
||||
"lint": "eslint ./src/**/*.js",
|
||||
"prettier": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'",
|
||||
"prettier:verify": "prettier --list-different '**/*.{css,html,js,json,md,yaml,yml}'",
|
||||
"serve": "lwc-services build && lwc-services serve",
|
||||
"serve:heroku": "lwc-services build --mode=production && lwc-services serve -i 0.0.0.0",
|
||||
"test:unit": "lwc-services test",
|
||||
"test:unit:coverage": "lwc-services test --coverage",
|
||||
"test:unit:debug": "lwc-services test --debug",
|
||||
"test:unit:watch": "lwc-services test --watch",
|
||||
"watch": "lwc-services watch",
|
||||
"watch:production": "lwc-services watch --mode=production"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Lightning Web Components Recipes</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="/resources/css/main.css" />
|
||||
<link rel="stylesheet" href="/resources/css/normalize.css" />
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="author" content="Salesforce Developer Evangelism" />
|
||||
<meta name="description" content="Lightning Web Components Recipes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-title"
|
||||
content="Lightning Web Components Recipes"
|
||||
/>
|
||||
<meta
|
||||
name="application-name"
|
||||
content="Lightning Web Components Recipes"
|
||||
/>
|
||||
<meta name="msapplication-TileColor" content="#2d89ef" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/resources/images/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/resources/images/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/resources/images/favicon-16x16.png"
|
||||
/>
|
||||
<link
|
||||
rel="mask-icon"
|
||||
href="/resources/images/safari-pinned-tab.svg"
|
||||
color="#5bbad5"
|
||||
/>
|
||||
<link rel="shortcut icon" href="/resources/images/favicon.ico" />
|
||||
<link rel="icon" href="/resources/images/logo.svg" />
|
||||
|
||||
<main class="wrapper">
|
||||
<a
|
||||
href="https://github.com/trailheadapps/lwc-recipes-oss"
|
||||
class="github-corner"
|
||||
target="_blank"
|
||||
aria-label="View source on GitHub"
|
||||
><svg
|
||||
width="65"
|
||||
height="65"
|
||||
viewBox="0 0 250 250"
|
||||
style="fill:#16325C; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"
|
||||
></path>
|
||||
<path
|
||||
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
fill="currentColor"
|
||||
style="transform-origin: 130px 106px;"
|
||||
class="octo-arm"
|
||||
></path>
|
||||
<path
|
||||
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
fill="currentColor"
|
||||
class="octo-body"
|
||||
></path></svg
|
||||
></a>
|
||||
<header class="header">
|
||||
<a class="logo" href="/">
|
||||
<div class="icon-logo">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 64 64"
|
||||
>
|
||||
<path fill="none" d="M0 0h64v64H0z" />
|
||||
<path
|
||||
fill="#00a1e0"
|
||||
d="M23 6h22l-8 18h11L20 58l6-26H16l7-26z"
|
||||
/>
|
||||
<path
|
||||
d="M20 60a2 2 0 0 1-1.95-2.45L23.49 34H16a2 2 0 0 1-1.93-2.52l7-26A2 2 0 0 1 23 4h22a2 2 0 0 1 1.83 2.81L40.08 22H48a2 2 0 0 1 1.54 3.27l-28 34A2 2 0 0 1 20 60zm-1.39-30H26a2 2 0 0 1 1.95 2.45l-4.09 17.72L43.76 26H37a2 2 0 0 1-1.83-2.81L41.92 8H24.53z"
|
||||
fill="#032e61"
|
||||
/>
|
||||
<path
|
||||
d="M26 26a2 2 0 0 1-1.93-2.53l3-11a2 2 0 1 1 3.86 1.05l-3 11A2 2 0 0 1 26 26z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="title">Lightning Web Components Recipes</h1>
|
||||
</a>
|
||||
<label class="menu-icon" for="menu-btn"
|
||||
><span class="navicon"></span
|
||||
></label>
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a
|
||||
href="https://lwc.dev/guide/introduction"
|
||||
target="_blank"
|
||||
>Guide</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://developer.salesforce.com/"
|
||||
target="_blank"
|
||||
>Salesforce Developers</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://trailhead.salesforce.com"
|
||||
target="_blank"
|
||||
>Trailhead</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<div id="main"></div>
|
||||
</main>
|
||||
</head>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
import { createElement, register } from 'lwc';
|
||||
import { registerWireService } from '@lwc/wire-service';
|
||||
import App from 'ui/app';
|
||||
|
||||
registerWireService(register);
|
||||
const app = createElement('ui-app', {
|
||||
is: App
|
||||
});
|
||||
// eslint-disable-next-line @lwc/lwc/no-document-query
|
||||
document.querySelector('#main').appendChild(app);
|
|
@ -0,0 +1,50 @@
|
|||
export const contacts = [
|
||||
{
|
||||
Id: '0031700000pJRRSAA4',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Email: 'amy@demo.net',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
},
|
||||
{
|
||||
Id: '0031700000pJRRTAA4',
|
||||
Name: 'Michael Jones',
|
||||
Title: 'VP of Sales',
|
||||
Phone: '4158526633',
|
||||
Email: 'michael@demo.net',
|
||||
Picture: '/resources/images/demo/michael_jones.jpg'
|
||||
},
|
||||
{
|
||||
Id: '0031700000pJRRUAA4',
|
||||
Name: 'Jennifer Wu',
|
||||
Title: 'CEO',
|
||||
Phone: '4158521463',
|
||||
Email: 'jennifer@demo.net',
|
||||
Picture: '/resources/images/demo/jennifer_wu.jpg'
|
||||
},
|
||||
{
|
||||
Id: '0031700000pJRRVAA4',
|
||||
Name: 'Anup Gupta',
|
||||
Title: 'VP of Products',
|
||||
Phone: '4158526398',
|
||||
Email: 'anup@demo.net',
|
||||
Picture: '/resources/images/demo/anup_gupta.jpg'
|
||||
},
|
||||
{
|
||||
Id: '0031700000pJRRWAA4',
|
||||
Name: 'Caroline Kingsley',
|
||||
Title: 'VP of Technology',
|
||||
Phone: '4158753654',
|
||||
Email: 'caroline@demo.net',
|
||||
Picture: '/resources/images/demo/caroline_kingsley.jpg'
|
||||
},
|
||||
{
|
||||
Id: '0031700000pJRRXAA4',
|
||||
Name: 'Jonathan Bradley',
|
||||
Title: 'VP of Opearations',
|
||||
Phone: '4158885522',
|
||||
Email: 'jonathan@demo.net',
|
||||
Picture: '/resources/images/demo/jonathan_bradley.jpg'
|
||||
}
|
||||
];
|
|
@ -0,0 +1,14 @@
|
|||
import { contacts } from 'data/contacts';
|
||||
|
||||
export function findContacts(searchKey) {
|
||||
if (searchKey.length === 0) return;
|
||||
const results = contacts.filter(
|
||||
item => item.Name.toLowerCase().indexOf(searchKey) !== -1
|
||||
);
|
||||
// eslint-disable-next-line consistent-return
|
||||
return results;
|
||||
}
|
||||
|
||||
export function getContactList() {
|
||||
return { data: contacts };
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { register, ValueChangedEvent } from '@lwc/wire-service';
|
||||
import { contacts } from 'data/contacts';
|
||||
|
||||
export default function getContactList() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
register(getContactList, eventTarget => {
|
||||
eventTarget.addEventListener('connect', () => {
|
||||
eventTarget.dispatchEvent(new ValueChangedEvent({ data: contacts }));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ApiFunction from 'recipe/apiFunction';
|
||||
|
||||
describe('recipe-api-function', () => {
|
||||
it('calls the public function "refresh" on the recipe-clock component', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-api-function', {
|
||||
is: ApiFunction
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Query ui-button component element
|
||||
const clockEl = element.shadowRoot.querySelector('recipe-clock');
|
||||
clockEl.refresh = jest.fn();
|
||||
|
||||
// Query ui-button element
|
||||
const buttonEl = element.shadowRoot.querySelector('ui-button');
|
||||
buttonEl.click();
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Compare if public function has been called
|
||||
expect(clockEl.refresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
recipe-clock {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 14px 8px 8px 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
recipe-clock:before {
|
||||
content: 'recipe-clock';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<ui-card title="ApiFunction">
|
||||
<div>
|
||||
<ui-button label="Refresh Time" onclick={handleRefresh}></ui-button>
|
||||
<recipe-clock></recipe-clock>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/apiFunction" slot="footer">
|
||||
Parent-to-child communication. Call a public (@api) function in a
|
||||
child component.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class ApiFunction extends LightningElement {
|
||||
handleRefresh() {
|
||||
this.template.querySelector('recipe-clock').refresh();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ApiProperty from 'recipe/apiProperty';
|
||||
|
||||
const PERCENTAGE_DEFAULT = 50;
|
||||
const PERCENTAGE_CUSTOM = 40;
|
||||
|
||||
describe('recipe-api-property', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders recipe-chart-bar component with a default percentage value', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-api-property', {
|
||||
is: ApiProperty
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Query chart-bar component
|
||||
const chartBarEl = element.shadowRoot.querySelector('recipe-chart-bar');
|
||||
expect(chartBarEl).not.toBeNull();
|
||||
|
||||
// Validation for default value passed down to child component
|
||||
expect(chartBarEl.percentage).toBe(PERCENTAGE_DEFAULT);
|
||||
});
|
||||
|
||||
it('changes the value of the recipe-chart-bar child component based on user input', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-api-property', {
|
||||
is: ApiProperty
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select input field for simulating user input
|
||||
const uiInputEl = element.shadowRoot.querySelector('ui-input');
|
||||
uiInputEl.value = PERCENTAGE_CUSTOM;
|
||||
uiInputEl.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
// Query chart-bar component
|
||||
const chartBarEl = element.shadowRoot.querySelector('recipe-chart-bar');
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Query newly set public property on chart-bar component
|
||||
expect(chartBarEl.percentage).toBe(PERCENTAGE_CUSTOM);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
recipe-chart-bar {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 10px 2px 2px 2px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
recipe-chart-bar:before {
|
||||
content: 'recipe-chart-bar';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<ui-card title="ApiProperty">
|
||||
<div>
|
||||
<ui-input
|
||||
label="Percentage"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={percentage}
|
||||
onchange={handlePercentageChange}
|
||||
></ui-input>
|
||||
<recipe-chart-bar percentage={percentage}></recipe-chart-bar>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/apiProperty" slot="footer">
|
||||
Parent-to-child communication. Pass data to a child component using
|
||||
its public (@api) properties.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,10 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class ApiProperty extends LightningElement {
|
||||
@track percentage = 50;
|
||||
|
||||
handlePercentageChange(event) {
|
||||
const percentage = event.target.value;
|
||||
this.percentage = percentage <= 100 ? percentage : 100;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ApiSetterGetter from 'recipe/apiSetterGetter';
|
||||
|
||||
describe('recipe-api-setter-getter', () => {
|
||||
it('creates a new todo item', () => {
|
||||
const TODO_DESCRIPTION = 'Some ToDo';
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-api-setter-getter', {
|
||||
is: ApiSetterGetter
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Query ui-input elements
|
||||
const uiInputEls = element.shadowRoot.querySelectorAll('ui-input');
|
||||
|
||||
const todoCountPrevious = element.shadowRoot.querySelector(
|
||||
'recipe-todo-list'
|
||||
).todos.length;
|
||||
|
||||
// Select input fields for simulating user input
|
||||
uiInputEls.forEach(el => {
|
||||
if (el.label === 'Description') {
|
||||
el.value = TODO_DESCRIPTION;
|
||||
} else if (el.label === 'Priority') {
|
||||
el.checked = true;
|
||||
}
|
||||
el.dispatchEvent(new CustomEvent('change'));
|
||||
});
|
||||
|
||||
// Select button for simulating click
|
||||
const buttonEl = element.shadowRoot.querySelector('ui-button');
|
||||
buttonEl.click();
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Compare if tracked property has been assigned a new value.
|
||||
const todoListEl = element.shadowRoot.querySelector(
|
||||
'recipe-todo-list'
|
||||
);
|
||||
expect(todoListEl.todos.length).toBe(todoCountPrevious + 1);
|
||||
expect(todoListEl.todos[2].description).toBe(TODO_DESCRIPTION);
|
||||
expect(todoListEl.todos[2].priority).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
recipe-todo-list {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 14px 8px 8px 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
recipe-todo-list:before {
|
||||
content: 'recipe-todo-list';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<ui-card title="ApiSetterGetter">
|
||||
<div>
|
||||
<ui-input
|
||||
label="Description"
|
||||
onchange={handleDescriptionChange}
|
||||
value={description}
|
||||
></ui-input>
|
||||
<ui-input
|
||||
label="Priority"
|
||||
type="checkbox"
|
||||
onchange={handlePriorityChange}
|
||||
checked={priority}
|
||||
></ui-input>
|
||||
<ui-button label="Add Todo" onclick={handleSave}></ui-button>
|
||||
<recipe-todo-list todos={todos}></recipe-todo-list>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/apiSetterGetter" slot="footer">
|
||||
Parent-to-child communication. Pass data to a child component using
|
||||
a public (@api) property implemented with a setter and getter, and
|
||||
apply some logic to the data as the property is being set.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,36 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class ApiSetterGetter extends LightningElement {
|
||||
lastTodoId = 2;
|
||||
|
||||
@track
|
||||
todos = [
|
||||
{ id: 1, description: 'Explore recipes', priority: true },
|
||||
{ id: 2, description: 'Install Ebikes sample app', priority: false }
|
||||
];
|
||||
|
||||
@track description;
|
||||
|
||||
@track priority = false;
|
||||
|
||||
handleDescriptionChange(event) {
|
||||
this.description = event.target.value;
|
||||
}
|
||||
|
||||
handlePriorityChange(event) {
|
||||
this.priority = event.target.checked;
|
||||
}
|
||||
|
||||
handleSave() {
|
||||
this.lastTodoId = this.lastTodoId + 1;
|
||||
// Using immutable data structures. Creating a new array with old and new items instead of mutating the existing array with push()
|
||||
this.todos = [
|
||||
...this.todos,
|
||||
{
|
||||
id: this.lastTodoId,
|
||||
description: this.description,
|
||||
priority: this.priority
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ChartBar from 'recipe/chartBar';
|
||||
|
||||
describe('recipe-chart-bar', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders a div with the percentage value as style attribute', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-chart-bar', {
|
||||
is: ChartBar
|
||||
});
|
||||
|
||||
// Set public property
|
||||
element.percentage = 40;
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Query div for validating computed style attribute value on component init
|
||||
const divEl = element.shadowRoot.querySelector('div.bar');
|
||||
expect(divEl).not.toBeNull();
|
||||
expect(divEl.style._values.width).toBe('40%');
|
||||
|
||||
// Set public property
|
||||
element.percentage = 60;
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Query div for validating computed style attribute value on public property change
|
||||
expect(divEl.style._values.width).toBe('60%');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
.container {
|
||||
overflow: hidden;
|
||||
color: #f5b041;
|
||||
display: flex;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 5px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin-left: 4px;
|
||||
height: 36px;
|
||||
background-color: #f5b041;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="text">{percentage}%</div>
|
||||
<div class="bar" style={style}></div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
import { LightningElement, api } from 'lwc';
|
||||
|
||||
export default class ChartBar extends LightningElement {
|
||||
@api percentage;
|
||||
|
||||
get style() {
|
||||
return `width: ${this.percentage}%`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { createElement } from 'lwc';
|
||||
import Clock from 'recipe/clock';
|
||||
|
||||
describe('recipe-clock', () => {
|
||||
it('sets current date/time after public function call', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-clock', {
|
||||
is: Clock
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Query ui-output element
|
||||
const uiDateTimeEl = element.shadowRoot.querySelector('ui-output');
|
||||
const currentDateTimeVal = uiDateTimeEl.value;
|
||||
|
||||
// Call public function on element
|
||||
element.refresh();
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Compare if tracked property has been assigned a new value.
|
||||
expect(uiDateTimeEl.value).not.toBe(currentDateTimeVal);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<ui-output value={timestamp}> </ui-output>
|
||||
</template>
|
|
@ -0,0 +1,11 @@
|
|||
import { LightningElement, api, track } from 'lwc';
|
||||
|
||||
export default class Clock extends LightningElement {
|
||||
@api
|
||||
refresh() {
|
||||
this.timestamp = new Date().toISOString();
|
||||
console.log(this.timestamp);
|
||||
}
|
||||
|
||||
@track timestamp = new Date().toISOString();
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { createElement } from 'lwc';
|
||||
import CompositionBasics from 'recipe/compositionBasics';
|
||||
|
||||
describe('recipe-composition-basics', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders one contact tile', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-composition-basics', {
|
||||
is: CompositionBasics
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select rendered contact tile for length check
|
||||
const contactTileEls = element.shadowRoot.querySelectorAll(
|
||||
'recipe-contact-tile'
|
||||
);
|
||||
expect(contactTileEls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders with contact tile properties set', () => {
|
||||
const USER_RESULT = 'Amy Taylor';
|
||||
const TITLE_RESULT = 'VP of Engineering';
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-composition-basics', {
|
||||
is: CompositionBasics
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select contact tile for public property check
|
||||
const contactTileEl = element.shadowRoot.querySelector(
|
||||
'recipe-contact-tile'
|
||||
);
|
||||
expect(contactTileEl.contact.Name).toBe(USER_RESULT);
|
||||
expect(contactTileEl.contact.Title).toBe(TITLE_RESULT);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
recipe-contact-tile {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
recipe-contact-tile:before {
|
||||
content: 'recipe-contact-tile';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<ui-card title="CompositionBasics">
|
||||
<div>
|
||||
<recipe-contact-tile contact={contact}></recipe-contact-tile>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/compositionBasics" slot="footer">
|
||||
Nest a child component into a parent component and pass data to the
|
||||
child component using its public (@api) properties.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,10 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class CompositionParent extends LightningElement {
|
||||
contact = {
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
};
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
recipe-contact-tile {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
padding: 14px 8px 8px 8px;
|
||||
}
|
||||
|
||||
recipe-contact-tile:before {
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
content: 'recipe-contact-tile';
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<ui-card title="CompositionContactSearch">
|
||||
<div>
|
||||
<ui-input
|
||||
type="search"
|
||||
onchange={handleKeyChange}
|
||||
label="Search"
|
||||
></ui-input>
|
||||
<template if:true={contacts}>
|
||||
<template for:each={contacts} for:item="contact">
|
||||
<recipe-contact-tile
|
||||
key={contact.Id}
|
||||
contact={contact}
|
||||
></recipe-contact-tile>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<template if:true={error}>
|
||||
<recipe-error-panel errors={error}></recipe-error-panel>
|
||||
</template>
|
||||
<recipe-view-source
|
||||
source="recipe/compositionContactSearch"
|
||||
slot="footer"
|
||||
>
|
||||
Create an experience component by assembling multiple child
|
||||
components. Type a few characters in the search bar to experience
|
||||
the recipe.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,25 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
import { findContacts } from 'data/simpleProvider';
|
||||
|
||||
/** The delay used when debouncing event handlers before a method call. */
|
||||
const DELAY = 350;
|
||||
|
||||
export default class CompositionContactSearch extends LightningElement {
|
||||
@track contacts;
|
||||
@track error;
|
||||
|
||||
handleKeyChange(event) {
|
||||
// Debouncing this method: Do not actually invoke the method call as long as this function is
|
||||
// being called within a delay of DELAY.
|
||||
window.clearTimeout(this.delayTimeout);
|
||||
const searchKey = event.target.value;
|
||||
// eslint-disable-next-line @lwc/lwc/no-async-operation
|
||||
this.delayTimeout = setTimeout(() => {
|
||||
try {
|
||||
this.contacts = findContacts(searchKey);
|
||||
} catch (e) {
|
||||
this.error = e;
|
||||
}
|
||||
}, DELAY);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { createElement } from 'lwc';
|
||||
import CompositionIteration from 'recipe/compositionIteration';
|
||||
|
||||
describe('recipe-composition-iteration', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders three contact tiles', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-composition-iteration', {
|
||||
is: CompositionIteration
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select rendered contact tile elements for length check
|
||||
const contactTileEls = element.shadowRoot.querySelectorAll(
|
||||
'recipe-contact-tile'
|
||||
);
|
||||
expect(contactTileEls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('renders contact tiles that contain specific names as contact tile data', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-composition-basics', {
|
||||
is: CompositionIteration
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select contact tiles for public property check
|
||||
const CONTACT_LIST_EXPECTED = [
|
||||
'Amy Taylor',
|
||||
'Michael Jones',
|
||||
'Jennifer Wu'
|
||||
];
|
||||
const contactTileNames = Array.from(
|
||||
element.shadowRoot.querySelectorAll('recipe-contact-tile')
|
||||
).map(contactTile => contactTile.contact.Name);
|
||||
expect(contactTileNames).toEqual(CONTACT_LIST_EXPECTED);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
recipe-contact-tile {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
recipe-contact-tile:before {
|
||||
content: 'recipe-contact-tile';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<ui-card title="CompositionIteration">
|
||||
<div>
|
||||
<template for:each={contacts} for:item="contact">
|
||||
<recipe-contact-tile
|
||||
key={contact.Id}
|
||||
contact={contact}
|
||||
></recipe-contact-tile>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/compositionIteration" slot="footer">
|
||||
Loop through an array of items in a template, and nest an instance
|
||||
of a child component for each item in the array.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,27 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class CompositionIteration extends LightningElement {
|
||||
contacts = [
|
||||
{
|
||||
Id: '003171931112854375',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
},
|
||||
{
|
||||
Id: '003192301009134555',
|
||||
Name: 'Michael Jones',
|
||||
Title: 'VP of Sales',
|
||||
Phone: '4158526633',
|
||||
Picture: '/resources/images/demo/michael_jones.jpg'
|
||||
},
|
||||
{
|
||||
Id: '003848991274589432',
|
||||
Name: 'Jennifer Wu',
|
||||
Title: 'CEO',
|
||||
Phone: '4158521463',
|
||||
Picture: '/resources/images/demo/jennifer_wu.jpg'
|
||||
}
|
||||
];
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ContactListItem from 'recipe/contactListItem';
|
||||
|
||||
describe('recipe-contact-list-item', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows contact name and image based on public property', () => {
|
||||
const CONTACT = {
|
||||
Id: '0031700000pJRRSAA4',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Email: 'amy@demo.net',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
};
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-contact-list-item', {
|
||||
is: ContactListItem
|
||||
});
|
||||
// Set public property
|
||||
element.contact = CONTACT;
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select elements for validation
|
||||
const imgEl = element.shadowRoot.querySelector('img');
|
||||
expect(imgEl.src).toContain(CONTACT.Picture);
|
||||
const nameEl = element.shadowRoot.querySelector('p');
|
||||
expect(nameEl.textContent).toBe(CONTACT.Name);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
:host {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
margin-left: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 6px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<a href="#" onclick={handleClick}>
|
||||
<img src={contact.Picture} alt="Profile photo" />
|
||||
<p>{contact.Name}</p>
|
||||
</a>
|
||||
</template>
|
|
@ -0,0 +1,16 @@
|
|||
import { LightningElement, api } from 'lwc';
|
||||
|
||||
export default class ContactListItem extends LightningElement {
|
||||
@api contact;
|
||||
|
||||
handleClick(event) {
|
||||
// 1. Prevent default behavior of anchor tag click which is to navigate to the href url
|
||||
event.preventDefault();
|
||||
// 2. Read about event best practices at http://lwc.dev/guide/events#pass-data-in-events
|
||||
const selectEvent = new CustomEvent('select', {
|
||||
detail: this.contact.Id
|
||||
});
|
||||
// 3. Fire the custom event
|
||||
this.dispatchEvent(selectEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ContactListItemBubbling from 'recipe/contactListItemBubbling';
|
||||
|
||||
describe('recipe-contact-list-item-bubbling', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows contact name and image based on public property', () => {
|
||||
const CONTACT = {
|
||||
Id: '0031700000pJRRSAA4',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Email: 'amy@demo.net',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
};
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-contact-list-item-bubbling', {
|
||||
is: ContactListItemBubbling
|
||||
});
|
||||
// Set public property
|
||||
element.contact = CONTACT;
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select elements for validation
|
||||
const imgEl = element.shadowRoot.querySelector('img');
|
||||
expect(imgEl.src).toContain(CONTACT.Picture);
|
||||
const nameEl = element.shadowRoot.querySelector('p');
|
||||
expect(nameEl.textContent).toBe(CONTACT.Name);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
:host {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
margin-left: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 8px 6px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<a href="#" onclick={handleSelect}>
|
||||
<img src={contact.Picture} alt="Profile photo" />
|
||||
<p>{contact.Name}</p>
|
||||
</a>
|
||||
</template>
|
|
@ -0,0 +1,16 @@
|
|||
import { LightningElement, api } from 'lwc';
|
||||
|
||||
export default class ContactListItemBubbling extends LightningElement {
|
||||
@api contact;
|
||||
|
||||
handleSelect(event) {
|
||||
// 1. Prevent default behavior of anchor tag click which is to navigate to the href url
|
||||
event.preventDefault();
|
||||
// 2. Create a custom event that bubbles. Read about event best practices at https://lwc.dev/guide/events#configure-event-propagation
|
||||
const selectEvent = new CustomEvent('contactselect', {
|
||||
bubbles: true
|
||||
});
|
||||
// 3. Fire the custom event
|
||||
this.dispatchEvent(selectEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ContactTile from 'recipe/contactTile';
|
||||
|
||||
const CONTACT_INPUT = {
|
||||
Id: '0031700000pJRRSAA4',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering',
|
||||
Phone: '4152568563',
|
||||
Email: 'amy@demo.net',
|
||||
Picture: '/resources/images/demo/amy_taylor.jpg'
|
||||
};
|
||||
|
||||
describe('recipe-contact-tile', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders picture, name, title, and phone number based on public property input', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-contact-tile', {
|
||||
is: ContactTile
|
||||
});
|
||||
// Set public property
|
||||
element.contact = CONTACT_INPUT;
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select elements for validation
|
||||
const imgEl = element.shadowRoot.querySelector('img');
|
||||
expect(imgEl.src).toContain(CONTACT_INPUT.Picture);
|
||||
|
||||
const detailEls = element.shadowRoot.querySelectorAll('p');
|
||||
expect(detailEls[0].textContent).toBe(CONTACT_INPUT.Name);
|
||||
expect(detailEls[1].textContent).toBe(CONTACT_INPUT.Title);
|
||||
|
||||
const phoneEl = element.shadowRoot.querySelector('ui-output');
|
||||
expect(phoneEl.value).toBe(CONTACT_INPUT.Phone);
|
||||
});
|
||||
|
||||
it('renders an informational message if public property is not set', () => {
|
||||
const MESSAGE = 'No contact data available.';
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-contact-tile', {
|
||||
is: ContactTile
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Select element for validation
|
||||
const detailEl = element.shadowRoot.querySelector('p');
|
||||
expect(detailEl.textContent).toBe(MESSAGE);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
:host {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<template if:true={contact}>
|
||||
<img src={contact.Picture} alt="Profile photo" />
|
||||
<p>{contact.Name}</p>
|
||||
<p>{contact.Title}</p>
|
||||
<p>
|
||||
<ui-output type="phone" value={contact.Phone}></ui-output>
|
||||
</p>
|
||||
</template>
|
||||
<template if:false={contact}
|
||||
><p>No contact data available.</p></template
|
||||
>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
import { LightningElement, api } from 'lwc';
|
||||
|
||||
export default class ContactTile extends LightningElement {
|
||||
@api contact;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { createElement } from 'lwc';
|
||||
import ErrorPanel from 'recipe/errorPanel';
|
||||
|
||||
describe('recipe-error-panel', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays a default friendly message', () => {
|
||||
const MESSAGE = 'Error retrieving data';
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-error-panel', {
|
||||
is: ErrorPanel
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
const messageEl = element.shadowRoot.querySelector('p');
|
||||
expect(messageEl.textContent).toBe(MESSAGE);
|
||||
});
|
||||
|
||||
it('displays a custom friendly message', () => {
|
||||
const MESSAGE = 'Errors are bad';
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-error-panel', {
|
||||
is: ErrorPanel
|
||||
});
|
||||
element.friendlyMessage = MESSAGE;
|
||||
document.body.appendChild(element);
|
||||
|
||||
const messageEl = element.shadowRoot.querySelector('p');
|
||||
expect(messageEl.textContent).toBe(MESSAGE);
|
||||
});
|
||||
|
||||
it('displays no error details when no errors are passed as parameters', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-error-panel', {
|
||||
is: ErrorPanel
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
const inputEl = element.shadowRoot.querySelector('ui-input');
|
||||
expect(inputEl).toBeNull();
|
||||
});
|
||||
|
||||
it('displays error details when errors are passed as parameters', () => {
|
||||
const ERROR_MESSAGES_INPUT = [
|
||||
{ statusText: 'First bad error' },
|
||||
{ statusText: 'Second bad error' }
|
||||
];
|
||||
const ERROR_MESSAGES_OUTPUT = ['First bad error', 'Second bad error'];
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-error-panel', {
|
||||
is: ErrorPanel
|
||||
});
|
||||
element.errors = ERROR_MESSAGES_INPUT;
|
||||
document.body.appendChild(element);
|
||||
|
||||
const inputEl = element.shadowRoot.querySelector('ui-input');
|
||||
inputEl.checked = true;
|
||||
inputEl.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
const messageTexts = Array.from(
|
||||
element.shadowRoot.querySelectorAll('p[class="error-message"]')
|
||||
).map(errorMessage => (errorMessage = errorMessage.textContent));
|
||||
expect(messageTexts).toEqual(ERROR_MESSAGES_OUTPUT);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
:host > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.padding-around-small {
|
||||
padding: 4px;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div class="padding-around-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
id="error"
|
||||
width="30px%"
|
||||
height="30px"
|
||||
>
|
||||
<path
|
||||
d="M12 .9C5.9.9.9 5.9.9 12s5 11.1 11.1 11.1 11.1-5 11.1-11.1S18.1.9 12 .9zM3.7 12c0-4.6 3.7-8.3 8.3-8.3 1.8 0 3.5.5 4.8 1.5L5.2 16.8c-1-1.3-1.5-3-1.5-4.8zm8.3 8.3c-1.8 0-3.5-.5-4.8-1.5L18.8 7.2c1 1.3 1.5 3 1.5 4.8 0 4.6-3.7 8.3-8.3 8.3z"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="padding-around-small">
|
||||
<p>{friendlyMessage}</p>
|
||||
<template if:true={errorMessages.length}>
|
||||
<div>
|
||||
<ui-input
|
||||
label="Show Details"
|
||||
type="checkbox"
|
||||
onchange={handleCheckboxChange}
|
||||
></ui-input>
|
||||
</div>
|
||||
<template if:true={viewDetails}>
|
||||
<template for:each={errorMessages} for:item="message">
|
||||
<p key={message} class="error-message">{message}</p>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,20 @@
|
|||
import { LightningElement, api, track } from 'lwc';
|
||||
import { reduceErrors } from 'recipe/ldsUtils';
|
||||
|
||||
export default class ErrorPanel extends LightningElement {
|
||||
/** Generic / user-friendly message */
|
||||
@api friendlyMessage = 'Error retrieving data';
|
||||
|
||||
@track viewDetails = false;
|
||||
|
||||
/** Single or array of errors */
|
||||
@api errors;
|
||||
|
||||
get errorMessages() {
|
||||
return reduceErrors(this.errors);
|
||||
}
|
||||
|
||||
handleCheckboxChange(event) {
|
||||
this.viewDetails = event.target.checked;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
recipe-contact-list-item-bubbling {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
padding: 14px 2px 0 2px;
|
||||
}
|
||||
|
||||
recipe-contact-list-item-bubbling:before {
|
||||
content: 'recipe-contact-list-item-bubbling';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
div.contacts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
padding-left: 10px;
|
||||
display: block;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.contact-details {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.contact-list,
|
||||
.contact-details {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<ui-card title="EventBubbling">
|
||||
<template if:true={contacts.data}>
|
||||
<div class="contacts">
|
||||
<!-- recipe-contact-list-item-bubbling emits a bubbling event so a single listener on a containing element works -->
|
||||
<div class="contact-list" oncontactselect={handleContactSelect}>
|
||||
<template for:each={contacts.data} for:item="contact">
|
||||
<recipe-contact-list-item-bubbling
|
||||
key={contact.Id}
|
||||
contact={contact}
|
||||
></recipe-contact-list-item-bubbling>
|
||||
</template>
|
||||
</div>
|
||||
<div class="contact-details">
|
||||
<template if:true={selectedContact}>
|
||||
<img
|
||||
src={selectedContact.Picture}
|
||||
alt="Profile photo"
|
||||
/>
|
||||
<p>{selectedContact.Name}</p>
|
||||
<p>{selectedContact.Title}</p>
|
||||
<p>
|
||||
<ui-output
|
||||
type="phone"
|
||||
value={selectedContact.Phone}
|
||||
></ui-output>
|
||||
</p>
|
||||
<p>
|
||||
<ui-output
|
||||
type="email"
|
||||
value={selectedContact.Email}
|
||||
></ui-output>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template if:true={contacts.error}>
|
||||
<recipe-error-panel errors={contacts.error}></recipe-error-panel>
|
||||
</template>
|
||||
<recipe-view-source source="recipe/eventBubbling" slot="footer">
|
||||
Child-to-grandparents communication using an event that bubbles and
|
||||
is handled on a higher level element in the DOM tree. Click an item
|
||||
in the list to see the recipe in action.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,12 @@
|
|||
import { LightningElement, wire, track } from 'lwc';
|
||||
import getContactList from 'data/wireGetContactListProvider';
|
||||
|
||||
export default class EventBubbling extends LightningElement {
|
||||
@track selectedContact;
|
||||
|
||||
@wire(getContactList) contacts;
|
||||
|
||||
handleContactSelect(event) {
|
||||
this.selectedContact = event.target.contact;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { createElement } from 'lwc';
|
||||
import EventSimple from 'recipe/eventSimple';
|
||||
|
||||
describe('recipe-event-simple', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('increments and decrements the page value by 1 on button click', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-event-simple', {
|
||||
is: EventSimple
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
const paginatorEl = element.shadowRoot.querySelector(
|
||||
'recipe-paginator'
|
||||
);
|
||||
const buttonEls = paginatorEl.shadowRoot.querySelectorAll('ui-button');
|
||||
|
||||
// First click "Next", so that the page property increments to 2
|
||||
buttonEls.forEach(buttonEl => {
|
||||
if (buttonEl.label === 'Next') {
|
||||
buttonEl.click();
|
||||
}
|
||||
});
|
||||
|
||||
const pageEl = element.shadowRoot.querySelector('p');
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// Verify that property is correctly incremented.
|
||||
expect(pageEl.textContent).toBe('Page 2');
|
||||
|
||||
// Now click "Previous", so that the page property decrements to 1
|
||||
buttonEls.forEach(buttonEl => {
|
||||
if (buttonEl.label === 'Previous') {
|
||||
buttonEl.click();
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that property is correctly incremented.
|
||||
expect(pageEl.textContent).toBe('Page 1');
|
||||
|
||||
// Decrement again
|
||||
buttonEls.forEach(buttonEl => {
|
||||
if (buttonEl.label === 'Previous') {
|
||||
buttonEl.click();
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Verify that property is not decremented, and the initial value stays on 1.
|
||||
expect(pageEl.textContent).toBe('Page 1');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
recipe-paginator {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
padding: 14px 2px 2px 2px;
|
||||
}
|
||||
|
||||
recipe-paginator:before {
|
||||
content: 'recipe-paginator';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<ui-card title="EventSimple">
|
||||
<div>
|
||||
<p class="center">Page {page}</p>
|
||||
<recipe-paginator
|
||||
onprevious={handlePrevious}
|
||||
onnext={handleNext}
|
||||
></recipe-paginator>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/eventSimple" slot="footer">
|
||||
Child-to-parent communication using a custom event.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class EventSimple extends LightningElement {
|
||||
@track page = 1;
|
||||
|
||||
handlePrevious() {
|
||||
if (this.page > 1) {
|
||||
this.page = this.page - 1;
|
||||
}
|
||||
}
|
||||
|
||||
handleNext() {
|
||||
this.page = this.page + 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
recipe-contact-list-item {
|
||||
position: relative;
|
||||
border: solid 1px #ecebea;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
padding: 14px 2px 0 2px;
|
||||
}
|
||||
|
||||
recipe-contact-list-item:before {
|
||||
content: 'recipe-contact-list-item';
|
||||
color: #dddbda;
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
left: 4px;
|
||||
background-color: #ffffff;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
div.contacts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
padding-left: 10px;
|
||||
display: block;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.contact-details {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.contact-list,
|
||||
.contact-details {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<ui-card title="EventWithData">
|
||||
<template if:true={contacts.data}>
|
||||
<div class="contacts">
|
||||
<!-- recipe-contact-list-item emits a non-bubbling event so each element must have a listener-->
|
||||
<div class="contact-list">
|
||||
<template for:each={contacts.data} for:item="contact">
|
||||
<recipe-contact-list-item
|
||||
key={contact.Id}
|
||||
contact={contact}
|
||||
onselect={handleSelect}
|
||||
></recipe-contact-list-item>
|
||||
</template>
|
||||
</div>
|
||||
<div class="contact-details">
|
||||
<template if:true={selectedContact}>
|
||||
<img
|
||||
src={selectedContact.Picture}
|
||||
alt="Profile photo"
|
||||
/>
|
||||
<p>{selectedContact.Name}</p>
|
||||
<p>{selectedContact.Title}</p>
|
||||
<p>
|
||||
<ui-output
|
||||
type="phone"
|
||||
value={selectedContact.Phone}
|
||||
></ui-output>
|
||||
</p>
|
||||
<p>
|
||||
<ui-output
|
||||
type="email"
|
||||
value={selectedContact.Email}
|
||||
></ui-output>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template if:true={contacts.error}>
|
||||
<recipe-error-panel errors={contacts.error}></recipe-error-panel>
|
||||
</template>
|
||||
<recipe-view-source source="recipe/eventWithData" slot="footer">
|
||||
Child-to-parent communication using a custom event that passes data
|
||||
to the parent component. Click an item in the list to see the recipe
|
||||
in action.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,15 @@
|
|||
import { LightningElement, wire, track } from 'lwc';
|
||||
import getContactList from 'data/wireGetContactListProvider';
|
||||
|
||||
export default class EventWithData extends LightningElement {
|
||||
@track selectedContact;
|
||||
|
||||
@wire(getContactList) contacts;
|
||||
|
||||
handleSelect(event) {
|
||||
const contactId = event.detail;
|
||||
this.selectedContact = this.contacts.data.find(
|
||||
contact => contact.Id === contactId
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { createElement } from 'lwc';
|
||||
import Hello from 'recipe/hello';
|
||||
|
||||
describe('recipe-hello', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays greeting', () => {
|
||||
// Create element
|
||||
const element = createElement('recipe-hello', {
|
||||
is: Hello
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify displayed greeting
|
||||
const div = element.shadowRoot.querySelector('div');
|
||||
expect(div.textContent).toBe('Hello, World!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<ui-card title="Hello">
|
||||
<div>
|
||||
Hello, {greeting}!
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/hello" slot="footer">
|
||||
Bind an HTML element to a component property.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,5 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class Hello extends LightningElement {
|
||||
greeting = 'World';
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { createElement } from 'lwc';
|
||||
import HelloBinding from 'recipe/helloBinding';
|
||||
|
||||
describe('recipe-hello-binding', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays greeting specified by change event target', () => {
|
||||
const EXPECTED = 'Test';
|
||||
|
||||
// Create element
|
||||
const element = createElement('recipe-hello-binding', {
|
||||
is: HelloBinding
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify default greeting
|
||||
let div = element.shadowRoot.querySelector('div');
|
||||
expect(div.textContent).not.toBe(`Hello, ${EXPECTED}!`);
|
||||
|
||||
// Trigger new greeting
|
||||
const inputEl = element.shadowRoot.querySelector('ui-input');
|
||||
inputEl.value = EXPECTED;
|
||||
inputEl.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Verify displayed greeting
|
||||
expect(div.textContent).toBe(`Hello, ${EXPECTED}!`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<ui-card title="HelloBinding">
|
||||
<div>
|
||||
<p>Hello, {greeting}!</p>
|
||||
<ui-input
|
||||
label="Name"
|
||||
value={greeting}
|
||||
onchange={handleChange}
|
||||
></ui-input>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/helloBinding" slot="footer">
|
||||
Change the value of a bound property when the value of an input
|
||||
field changes. Type something in the input field to see the recipe
|
||||
in action.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class HelloBinding extends LightningElement {
|
||||
@track greeting = 'World';
|
||||
|
||||
handleChange(event) {
|
||||
this.greeting = event.target.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { createElement } from 'lwc';
|
||||
import HelloConditionalRendering from 'recipe/helloConditionalRendering';
|
||||
|
||||
describe('recipe-hello-conditional-rendering', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not show details by default', () => {
|
||||
// Create element
|
||||
const element = createElement('recipe-hello-conditional-rendering', {
|
||||
is: HelloConditionalRendering
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify displayed message
|
||||
const detailEl = element.shadowRoot.querySelector('.details');
|
||||
expect(detailEl.textContent).toBe('Not showing details.');
|
||||
});
|
||||
|
||||
it('shows details when checkbox toggled', () => {
|
||||
// Create element
|
||||
const element = createElement('recipe-hello-conditional-rendering', {
|
||||
is: HelloConditionalRendering
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Toggle checkbox to show details
|
||||
const inputEl = element.shadowRoot.querySelector('ui-input');
|
||||
inputEl.checked = true;
|
||||
inputEl.dispatchEvent(new CustomEvent('change'));
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Verify displayed message
|
||||
const detailEl = element.shadowRoot.querySelector('.details');
|
||||
expect(detailEl.textContent).toBe('These are the details!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<ui-card title="HelloConditionalRendering">
|
||||
<div>
|
||||
<ui-input
|
||||
type="checkbox"
|
||||
label="Show details"
|
||||
onchange={handleChange}
|
||||
></ui-input>
|
||||
|
||||
<div class="details">
|
||||
<template if:true={areDetailsVisible}>
|
||||
These are the details!
|
||||
</template>
|
||||
<template if:false={areDetailsVisible}>
|
||||
Not showing details.
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<recipe-view-source
|
||||
source="recipe/helloConditionalRendering"
|
||||
slot="footer"
|
||||
>
|
||||
Conditionally render elements.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class HelloConditionalRendering extends LightningElement {
|
||||
@track areDetailsVisible = false;
|
||||
|
||||
handleChange(event) {
|
||||
this.areDetailsVisible = event.target.checked;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { createElement } from 'lwc';
|
||||
import HelloExpressions from 'recipe/helloExpressions';
|
||||
|
||||
const PREFIX = 'Uppercased Full Name:';
|
||||
|
||||
describe('recipe-hello-expressions', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
function setInputElementValues(element, firstName, lastName) {
|
||||
// ui-input doesn't mirror its properties as attributes so
|
||||
// can't use an attribute query selector.
|
||||
element.shadowRoot.querySelectorAll('ui-input').forEach(input => {
|
||||
if (firstName && input.name === 'firstName') {
|
||||
input.value = firstName;
|
||||
input.dispatchEvent(new CustomEvent('change'));
|
||||
} else if (lastName && input.name === 'lastName') {
|
||||
input.value = lastName;
|
||||
input.dispatchEvent(new CustomEvent('change'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
it('displays first name as uppercase', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-expressions', {
|
||||
is: HelloExpressions
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
setInputElementValues(element, 'Peter', undefined);
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Verify displayed message
|
||||
const detailEl = element.shadowRoot.querySelector('p');
|
||||
expect(detailEl.textContent).toBe(`${PREFIX} PETER`);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays last name as uppercase', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-expressions', {
|
||||
is: HelloExpressions
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
setInputElementValues(element, undefined, 'Pan');
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Verify displayed message
|
||||
const detailEl = element.shadowRoot.querySelector('p');
|
||||
expect(detailEl.textContent).toBe(`${PREFIX} PAN`);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays first and last name as uppercase', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-expressions', {
|
||||
is: HelloExpressions
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
setInputElementValues(element, 'Peter', 'Pan');
|
||||
|
||||
// Return a promise to wait for any asynchronous DOM updates. Jest
|
||||
// will automatically wait for the Promise chain to complete before
|
||||
// ending the test and fail the test if the promise rejects.
|
||||
return Promise.resolve().then(() => {
|
||||
// Verify displayed message
|
||||
const detailEl = element.shadowRoot.querySelector('p');
|
||||
expect(detailEl.textContent).toBe(`${PREFIX} PETER PAN`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
.margin-top-medium {
|
||||
margin: 8 0 0 0;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<ui-card title="HelloExpressions">
|
||||
<div>
|
||||
<ui-input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
onchange={handleChange}
|
||||
></ui-input>
|
||||
<ui-input
|
||||
name="lastName"
|
||||
label="Last Name"
|
||||
onchange={handleChange}
|
||||
></ui-input>
|
||||
<p class="margin-top-medium">
|
||||
Uppercased Full Name: {uppercasedFullName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<recipe-view-source source="recipe/helloExpressions" slot="footer">
|
||||
Use JavaScript expressions in a template. Type something in the
|
||||
input fields to see the recipe in action.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,19 @@
|
|||
import { LightningElement, track } from 'lwc';
|
||||
|
||||
export default class HelloExpressions extends LightningElement {
|
||||
@track firstName = '';
|
||||
@track lastName = '';
|
||||
|
||||
handleChange(event) {
|
||||
const field = event.target.name;
|
||||
if (field === 'firstName') {
|
||||
this.firstName = event.target.value;
|
||||
} else if (field === 'lastName') {
|
||||
this.lastName = event.target.value;
|
||||
}
|
||||
}
|
||||
|
||||
get uppercasedFullName() {
|
||||
return `${this.firstName} ${this.lastName}`.trim().toUpperCase();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { createElement } from 'lwc';
|
||||
import HelloForEach from 'recipe/helloForEach';
|
||||
|
||||
describe('recipe-hello-for-each', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays contacts in specific order', () => {
|
||||
const EXPECTED = [
|
||||
'Amy Taylor, VP of Engineering',
|
||||
'Michael Jones, VP of Sales',
|
||||
'Jennifer Wu, CEO'
|
||||
];
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-for-each', {
|
||||
is: HelloForEach
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify displayed list
|
||||
const contacts = Array.from(
|
||||
element.shadowRoot.querySelectorAll('li')
|
||||
).map(li => li.textContent);
|
||||
expect(contacts).toEqual(EXPECTED);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<ui-card title="HelloForEach">
|
||||
<ul>
|
||||
<template for:each={contacts} for:item="contact">
|
||||
<li key={contact.Id}>
|
||||
{contact.Name}, {contact.Title}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
<recipe-view-source source="recipe/helloForEach" slot="footer">
|
||||
Loop through an array of items in a template.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class HelloForEach extends LightningElement {
|
||||
contacts = [
|
||||
{
|
||||
Id: '003171931112854375',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering'
|
||||
},
|
||||
{
|
||||
Id: '003192301009134555',
|
||||
Name: 'Michael Jones',
|
||||
Title: 'VP of Sales'
|
||||
},
|
||||
{
|
||||
Id: '003848991274589432',
|
||||
Name: 'Jennifer Wu',
|
||||
Title: 'CEO'
|
||||
}
|
||||
];
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { createElement } from 'lwc';
|
||||
import HelloIterator from 'recipe/helloIterator';
|
||||
|
||||
describe('recipe-hello-iterator', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays contacts in specific order', () => {
|
||||
const EXPECTED = [
|
||||
'Amy Taylor, VP of Engineering',
|
||||
'Michael Jones, VP of Sales',
|
||||
'Jennifer Wu, CEO'
|
||||
];
|
||||
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-iterator', {
|
||||
is: HelloIterator
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify displayed list
|
||||
const contacts = Array.from(
|
||||
element.shadowRoot.querySelectorAll('li')
|
||||
).map(li => li.textContent);
|
||||
expect(contacts).toEqual(EXPECTED);
|
||||
});
|
||||
|
||||
it('displays div in first and last contacts', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-hello-iterator', {
|
||||
is: HelloIterator
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Verify first ul's first child is a div
|
||||
expect(
|
||||
element.shadowRoot.querySelector('ul:first-child').firstChild
|
||||
.tagName
|
||||
).toBe('DIV');
|
||||
// Verify last li's last child is a div
|
||||
expect(
|
||||
element.shadowRoot.querySelector('li:last-child').lastChild.tagName
|
||||
).toBe('DIV');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
.list-first {
|
||||
border-top: 1px solid #706e6b;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.list-last {
|
||||
border-bottom: 1px solid #706e6b;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<ui-card title="HelloIterator">
|
||||
<ul>
|
||||
<template iterator:it={contacts}>
|
||||
<div
|
||||
if:true={it.first}
|
||||
key={it.value.Id}
|
||||
class="list-first"
|
||||
></div>
|
||||
<li key={it.value.Id}>
|
||||
{it.value.Name}, {it.value.Title}
|
||||
<div if:true={it.last} class="list-last"></div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<recipe-view-source source="recipe/helloIterator" slot="footer">
|
||||
Loop through an array with special behavior for the first and last
|
||||
items.
|
||||
</recipe-view-source>
|
||||
</ui-card>
|
||||
</template>
|
|
@ -0,0 +1,21 @@
|
|||
import { LightningElement } from 'lwc';
|
||||
|
||||
export default class HelloIterator extends LightningElement {
|
||||
contacts = [
|
||||
{
|
||||
Id: '003171931112854375',
|
||||
Name: 'Amy Taylor',
|
||||
Title: 'VP of Engineering'
|
||||
},
|
||||
{
|
||||
Id: '003192301009134555',
|
||||
Name: 'Michael Jones',
|
||||
Title: 'VP of Sales'
|
||||
},
|
||||
{
|
||||
Id: '003848991274589432',
|
||||
Name: 'Jennifer Wu',
|
||||
Title: 'CEO'
|
||||
}
|
||||
];
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Reduces one or more LDS errors into a string[] of error messages.
|
||||
* @param {FetchResponse|FetchResponse[]} errors
|
||||
* @return {String[]} Error messages
|
||||
*/
|
||||
export function reduceErrors(errors) {
|
||||
if (!Array.isArray(errors)) {
|
||||
errors = [errors];
|
||||
}
|
||||
|
||||
return (
|
||||
errors
|
||||
// Remove null/undefined items
|
||||
.filter(error => !!error)
|
||||
// Extract an error message
|
||||
.map(error => {
|
||||
// UI API read errors
|
||||
if (Array.isArray(error.body)) {
|
||||
return error.body.map(e => e.message);
|
||||
}
|
||||
// UI API DML, Apex and network errors
|
||||
else if (error.body && typeof error.body.message === 'string') {
|
||||
return error.body.message;
|
||||
}
|
||||
// JS errors
|
||||
else if (typeof error.message === 'string') {
|
||||
return error.message;
|
||||
}
|
||||
// Unknown error shape so try HTTP status text
|
||||
return error.statusText;
|
||||
})
|
||||
// Flatten
|
||||
.reduce((prev, curr) => prev.concat(curr), [])
|
||||
// Remove empty strings
|
||||
.filter(message => !!message)
|
||||
);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { createElement } from 'lwc';
|
||||
import LibsChartjs from 'recipe/libsChartjs';
|
||||
|
||||
describe('recipe-libs-chartjs', () => {
|
||||
afterEach(() => {
|
||||
// The jsdom instance is shared across test cases in a single file so reset the DOM
|
||||
while (document.body.firstChild) {
|
||||
document.body.removeChild(document.body.firstChild);
|
||||
}
|
||||
// Clear mocks so that every test run has a clean implementation
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('contains a canvas element for ChartJs', () => {
|
||||
// Create initial element
|
||||
const element = createElement('recipe-libs-chartjs', {
|
||||
is: LibsChartjs
|
||||
});
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Querying the DOM element that has the lwc:dom directive set.
|
||||
const domEl = element.shadowRoot.querySelector('canvas[class="donut"]');
|
||||
expect(domEl).not.toBeNull();
|
||||
});
|
||||
});
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче