├── .eslintignore ├── .forceignore ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── config └── project-scratch-def.json ├── force-app └── main │ └── default │ ├── aura │ └── .eslintrc.json │ ├── classes │ ├── DirectiveExampleController.cls │ └── DirectiveExampleController.cls-meta.xml │ ├── lwc │ ├── .eslintrc.json │ ├── directiveComparator │ │ ├── __tests__ │ │ │ └── directiveComparator.test.js │ │ ├── directiveComparator.js │ │ └── directiveComparator.js-meta.xml │ ├── directiveComparatorContactSearchExample │ │ ├── __tests__ │ │ │ └── directiveComparatorContactSearchExample.test.js │ │ ├── directiveComparatorContactSearchExample.html │ │ ├── directiveComparatorContactSearchExample.js │ │ └── directiveComparatorContactSearchExample.js-meta.xml │ ├── directiveComparatorExample │ │ ├── __tests__ │ │ │ └── directiveComparatorExample.test.js │ │ ├── directiveComparatorExample.html │ │ ├── directiveComparatorExample.js │ │ └── directiveComparatorExample.js-meta.xml │ ├── directiveComparatorIterationExample │ │ ├── __tests__ │ │ │ └── directiveComparatorIterationExample.test.js │ │ ├── directiveComparatorIterationExample.html │ │ ├── directiveComparatorIterationExample.js │ │ └── directiveComparatorIterationExample.js-meta.xml │ └── directiveComparatorSimpleExample │ │ ├── __tests__ │ │ └── directiveComparatorSimpleExample.test.js │ │ ├── directiveComparatorSimpleExample.html │ │ ├── directiveComparatorSimpleExample.js │ │ └── directiveComparatorSimpleExample.js-meta.xml │ └── tabs │ └── Directive_Example.tab-meta.xml ├── jest.config.js ├── package-lock.json ├── package.json └── sfdx-project.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | **/aura/**/*.auradoc 7 | **/aura/**/*.cmp 8 | **/aura/**/*.css 9 | **/aura/**/*.design 10 | **/aura/**/*.evt 11 | **/aura/**/*.json 12 | **/aura/**/*.svg 13 | **/aura/**/*.tokens 14 | **/aura/**/*.xml 15 | **/aura/**/*.app 16 | .sfdx 17 | -------------------------------------------------------------------------------- /.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** 13 | 14 | # Other configuration files 15 | **/*.profile-meta.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sf/ 7 | .sfdx/ 8 | .localdevserver/ 9 | deploy-options.json 10 | 11 | # LWC VSCode autocomplete 12 | **/lwc/jsconfig.json 13 | 14 | # LWC Jest coverage reports 15 | coverage/ 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Dependency directories 25 | node_modules/ 26 | 27 | # Eslint cache 28 | .eslintcache 29 | 30 | # MacOS system files 31 | .DS_Store 32 | 33 | # Windows system files 34 | Thumbs.db 35 | ehthumbs.db 36 | [Dd]esktop.ini 37 | $RECYCLE.BIN/ 38 | 39 | # Local environment variables 40 | .env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run precommit -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | .vscode 9 | 10 | coverage/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "salesforce.salesforcedx-vscode", 4 | "redhat.vscode-xml", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "financialforce.lana" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Apex Replay Debugger", 9 | "type": "apex-replay", 10 | "request": "launch", 11 | "logFile": "${command:AskForLogFileName}", 12 | "stopOnEntry": true, 13 | "trace": true 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/.sfdx": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Shinichi Tomita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Directive Comparator for Lightning Web Components 2 | 3 | Directive Comparator for Lightning Web Components is an utility that allows property comparison in the template HTML directive. 4 | It removes ugly getters from your class files, and keeps the template files to be self-descriptive. 5 | 6 | ## Current Problem 7 | 8 | If you have property `rank` and `fullName` in your component, and want to show special message when the `rank` value is something special, you can do it in Aura as follows: 9 | 10 | ```html 11 | 12 | 13 |
14 | 15 | Hi, {!v.fullName} - special offer to you, click here. 16 | 17 | 18 | Hi, {!v.fullName}, thanks for visiting again ! 19 | 20 | 21 | Welcome, {!v.fullName} 22 | 23 |
24 |
25 | ``` 26 | 27 | Unlike Aura component, LWC does not allow the inline expression in template, so comparisons should be written in the script file. 28 | You have to move the property comparison expression to the getter function of the component class. 29 | 30 | ```javascript 31 | import { LightningElement } from 'lwc'; 32 | 33 | export default class MyComponent1 extends LightningElement { 34 | rank; 35 | 36 | fullName; 37 | 38 | get isGoldRank() { 39 | return this.rank === 'gold'; 40 | } 41 | 42 | get isSilverRank() { 43 | return this.rank === 'gold'; 44 | } 45 | 46 | get isBronzeRank() { 47 | return this.rank === 'bronze'; 48 | } 49 | } 50 | ``` 51 | 52 | ```html 53 | 66 | ``` 67 | 68 | It is very daunting when it comes to comparing within an array loop. 69 | The array must be converted to include the comparison results for each item. 70 | 71 | ```javascript 72 | import { LightningElement } from 'lwc'; 73 | 74 | export default class MyComponent2 extends LightningElement { 75 | customerId = 1; 76 | 77 | customers_ = [ 78 | { id: 1, fullName: 'John Doe', rank: 'gold' }, 79 | { id: 2, fullName: 'Amy Taylor', rank: 'silver' }, 80 | { id: 3, fullName: 'Michael Jones', rank: 'bronze' }, 81 | { id: 4, fullName: 'Jane Doe', rank: 'silver' }, 82 | ]; 83 | 84 | get customers() { 85 | return this.customers_.map((customer) => ({ 86 | ...customer, 87 | isSelected: customer.id === this.customerId, 88 | isGoldRank: customer.rank === 'gold', 89 | isSilverRank: customer.rank === 'silver', 90 | isBronzeRank: customer.rank === 'bronze', 91 | }); 92 | } 93 | } 94 | ``` 95 | 96 | ```html 97 | 124 | ``` 125 | 126 | This is due to the philosophy of Lightning Web Components that the logic should be separated from the template, but this tends to make components less prospective. 127 | 128 | ## Solution: Directive Comparator 129 | 130 | Directive Comparator for Lightning Web Components solves the above concerns. 131 | 132 | Remove getters, just add a property to the class, with an initial (and invariant) value generated by the `comparator` function. 133 | 134 | ```javascript 135 | import { LightningElement } from "lwc"; 136 | import { comparator } from "c/directiveComparator"; 137 | 138 | export default class DirectiveComparatorSimpleExample extends LightningElement { 139 | rank; 140 | 141 | fullName; 142 | 143 | $ = comparator(this, { 144 | rank: ["gold", "silver", "bronze"] 145 | }); 146 | } 147 | ``` 148 | 149 | The template markup goes like this. Note that it does not have any getters in the class. 150 | 151 | ```html 152 | 165 | ``` 166 | 167 | You can do the comparison in iterations, too. 168 | Each iteration element has a comparator property to form a comparison expression. 169 | 170 | ```javascript 171 | import { LightningElement } from "lwc"; 172 | import { comparator, NUMBER_VALUE } from "c/directiveComparator"; 173 | 174 | export default class DirectiveComparatorIterationExample extends LightningElement { 175 | customerId = 1; 176 | 177 | customers = [ 178 | { id: 1, fullName: "John Doe", rank: "gold" }, 179 | { id: 2, fullName: "Amy Taylor", rank: "silver" }, 180 | { id: 3, fullName: "Michael Jones", rank: "bronze" }, 181 | { id: 4, fullName: "Jane Doe", rank: "silver" } 182 | ]; 183 | 184 | $ = comparator(this, { 185 | customerId: NUMBER_VALUE, 186 | customers: [ 187 | { 188 | id: NUMBER_VALUE, 189 | rank: ["gold", "silver", "bronze"] 190 | } 191 | ] 192 | }); 193 | } 194 | ``` 195 | 196 | ```html 197 | 233 | ``` 234 | 235 | ## Usage 236 | 237 | ### Declaration 238 | 239 | To use the Directive Comparator, import the `compare` function from `c/directiveCompoarator`. 240 | This function is supposed to use the function with a class field declaration. 241 | 242 | ```javascript 243 | import { LightningElement } from "lwc"; 244 | import { comparator } from "c/directiveComparator"; 245 | 246 | export default class MyComponent extends LightningElement { 247 | prop1; 248 | prop2 = 123; 249 | // ... other field declarations ... 250 | 251 | // use in field declaration 252 | $ = comparator(this, { 253 | /* ... */ 254 | }); 255 | 256 | // ... method declarations ... 257 | } 258 | ``` 259 | 260 | The `compare` function accepts three parameters, `context`, `contextType`, and `options`. 261 | 262 | The `context` is the root object of the properties to compare. It is supposed to refer to the component instance, so pass `this` in the first argument. 263 | 264 | The `contextType` is a structure definition of the properties which you want to compare in the template. Primitive properties can be expressed by `STRING_VALUE`, `NUMBER_VALUE`, or `BOOLEAN_VALUE`. 265 | If the property is an object or an array, the definition also will nest to sub object / array. 266 | 267 | ```javascript 268 | import { LightningElement } from "lwc"; 269 | import { 270 | comparator, 271 | NUMBER_VALUE, 272 | STRING_VALUE, 273 | BOOLEAN_VALUE, 274 | ANY_VALUE 275 | } from "c/directiveComparator"; 276 | 277 | export default class MyComponent extends LightningElement { 278 | prop1 = 1; 279 | prop2 = "abc"; 280 | prop3 = null; 281 | object1 = { 282 | foo: "FOO", 283 | bar: "BAR" 284 | }; 285 | array1 = []; 286 | 287 | $ = comparator(this, { 288 | prop1: NUMBER_VALUE, 289 | prop2: STRING_VALUE, 290 | prop3: ANY_VALUE, 291 | object: { 292 | foo: STRING_VALUE, 293 | bar: STRING_VALUE 294 | }, 295 | array: [ 296 | { 297 | id: STRING_VALUE, 298 | active: BOOLEAN_VALUE 299 | } 300 | ] 301 | }); 302 | } 303 | ``` 304 | 305 | You can omit the `contextType` in argument. If the `contextType` is omitted, it will scan all properties defined in the class and estimate their type information. 306 | 307 | Even if it can be omitted, the estimation runs only in initialization phase, so the estimation will not be perfect. It is recommended to pass `contextType` argument as much as possible for stable usage. 308 | 309 | ```javascript 310 | export default class MyComponent extends LightningElement { 311 | prop1 = 1; 312 | prop2 = "abc"; 313 | // ... 314 | 315 | // the comarator field declaration should come to the last in the field declarations. 316 | $ = comparator(this); 317 | } 318 | ``` 319 | 320 | ### Directives in Template 321 | 322 | When you have attributes in the template to bind comparison result (for example, `lwc:if`), you can use the comparator declared in the previous step instead of directly referring the properties in the class. 323 | 324 | For example, if you want to check the `prop1` is greater than 1, you can write the template like this. 325 | 326 | ```html 327 | 334 | ``` 335 | 336 | In above template, the part of `$.prop1` is comparator property which refers context's `prop1` property value, and the `gt` is comparison operator, and `one` is the pre-defined constant value used as operand. 337 | 338 | If you are using iteration in template, don't warry. Directive Comparator supports that usage. 339 | 340 | Consider that the following class defined: 341 | 342 | ```javascript 343 | export default class MyComponent extends LightningElement { 344 | contactId = "c01"; 345 | 346 | contacts = [ 347 | { Id: "c01", Name: "John Doe" }, 348 | { Id: "c02", Name: "Amy Taylor" } 349 | //... 350 | ]; 351 | 352 | $ = comparator(this, { 353 | contactId: STRING_VALUE, 354 | contacts: [ 355 | { 356 | Id: STRING_VALUE, 357 | Name: STRING_VALUE 358 | } 359 | ] 360 | }); 361 | } 362 | ``` 363 | 364 | Template to iterate the contacts becomes: 365 | 366 | ```html 367 | 379 | ``` 380 | 381 | Above template, The `$.contacts` directive is used in `for:each` attribute instead of `contacts` for iterating contact list. This iterator gives additional property `$` to each iteration item, which is a comparator object for properties of the iteration item. 382 | 383 | In the iteration loop, the template conditionally displays information by using `lwc:if`, and the condition is described as `contact.$.Id.equals.$contactId`. The part of `contact.$.Id` is comparator property to refer the `Id` property of `contact`. The `equals` represents equality operator. The `$contactId` refers root context property - that is, `contactId` field in the component. 384 | 385 | ### Operators 386 | 387 | There are pre-defined operators to form comparison directive. Followings are the available operators: 388 | 389 | * **is / equals** - Checks if given two values are exactly equal or not. 390 | 391 | * **isNot / notEquals** - Checks if given two values are not exactly equal. 392 | 393 | * **gt / greaterThan** - Checks if the comparing property's value is greater than the comparing value. 394 | 395 | * **gte / greaterThanOrEquals** - Checks if the comparing property's value is greater than or equals to the comparing value. 396 | 397 | * **lt / lessThan** - Checks if the comparing property's value is less than the comparing value. 398 | 399 | * **lte / lessThanOrEquals** - Checks if the comparing property's value is less than or equals to the comparing value. 400 | 401 | * **startsWith** - Checks if the comparing property's value (string) starts with the comparing string value. 402 | 403 | * **endsWith** - Checks if the comparing property's value (string) ends with the comparing string value. 404 | 405 | * **includes** - Checks if the comparing property's value (string) includes the comparing string value. 406 | 407 | * **isTruthy / isFalsy** - Checks if the comparing property's value is truthy / falsy. 408 | 409 | * **isNull / isNotNull** - Checks if the comparing property's value is null or not null. 410 | 411 | * **isUndefined / isNotUndefined** - Checks if the comparing property's value is undefined in JavaScript or not. 412 | 413 | * **isNullish / isNotNullish** - Checks if the comparing property's value is undefined or null in JavaScript. 414 | 415 | * **isEmpty / isNotEmpty** - Checks if the comparing property's value is undefined, null, empty string, or empty array in JavaScript. 416 | 417 | * **not** - Operatior that negates following comparison result. For example, `$.prop1.not.startsWith.foo` negates the comparison result from `prop1` property value and constant `foo` using operator `startsWith`. 418 | 419 | ### Constants 420 | 421 | You might need to compare the property with constant value like 0, "foo", true, or null. 422 | You can declare which constant values can be used in the comparision directive in the type definition. 423 | In the property type definition you can pass the list of possible constant values in the array. 424 | 425 | ```javascript 426 | export default class MyComponent extends LightningElement { 427 | type = "customer"; 428 | 429 | $ = comparator(this, { 430 | type: ["customer", "partner", "competitor"], 431 | }); 432 | } 433 | ``` 434 | 435 | ```html 436 | 443 | ``` 444 | 445 | If you want to pass numbers or texts that has prohibited chars in lwc directive, you can pass it in name-value pair (tupple). 446 | 447 | ```javascript 448 | export default class MyComponent extends LightningElement { 449 | type = "01. Customer"; 450 | limit = 10; 451 | 452 | $ = comparator(this, { 453 | type: [ 454 | ["customer", "01. Customer"], 455 | ["partner", "02. Partner"], 456 | ["competitor", "03. Competitor"] 457 | ], 458 | limit: [ 459 | ["ten", 10], 460 | ["twenty", 20] 461 | ] 462 | }); 463 | } 464 | ``` 465 | 466 | ```html 467 | 477 | ``` 478 | 479 | #### Global Constants 480 | 481 | If there are constants widely used in the component properties, pass them to `constants` in `options` argument in `comparator` function. 482 | 483 | ```javascript 484 | export default class PersonComponent extends LightningElement { 485 | name = "Michael Johnson"; 486 | title = "CEO"; 487 | 488 | $ = comparator( 489 | this, 490 | { 491 | name: STRING_VALUE, 492 | title: STRING_VALUE 493 | }, 494 | { 495 | constants: { 496 | min: 1, 497 | max: 255 498 | } 499 | } 500 | ); 501 | } 502 | ``` 503 | 504 | ```html 505 | 527 | ``` 528 | 529 | #### Pre-defined Constants 530 | 531 | There are pre-defined constants that can be used without declarations: 532 | 533 | * zero 534 | * one 535 | * true 536 | * false 537 | * null 538 | * undefined 539 | 540 | 541 | ### Context Property Reference 542 | 543 | It is possible to reference root context properties in comparison operand, that is, the value of the fields in the component. They are referernced by $-prefixed name in the operand. 544 | 545 | ```javascript 546 | export default class MyComponent extends LightningElement { 547 | selected = 2; 548 | 549 | fruits = [{ 550 | id: 1, 551 | name: "apple" 552 | }, { 553 | id: 2, 554 | name: "orange" 555 | }, { 556 | id: 3, 557 | name: "melon" 558 | }, { 559 | id: 4, 560 | name: "banana" 561 | }]; 562 | 563 | $ = comparator(this, { 564 | selected: NUMBER_VALUE, 565 | fruits: [{ 566 | id: NUMBER_VALUE, 567 | name: STRING_VALUE 568 | }], 569 | }); 570 | } 571 | ``` 572 | 573 | ```html 574 | 586 | ``` 587 | 588 | In the above template, the `$selected` is used in the `fruit.$.id.is` comparison, meaning that it is referencing `selected` field value in the component. 589 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "stomita company", 3 | "edition": "Developer", 4 | "features": ["EnableSetPasswordInApi"], 5 | "settings": { 6 | "lightningExperienceSettings": { 7 | "enableS1DesktopEnabled": true 8 | }, 9 | "mobileSettings": { 10 | "enableS1EncryptedStoragePref2": false 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /force-app/main/default/aura/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@salesforce/eslint-plugin-aura"], 3 | "extends": ["plugin:@salesforce/eslint-plugin-aura/recommended"], 4 | "rules": { 5 | "vars-on-top": "off", 6 | "no-unused-expressions": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DirectiveExampleController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class DirectiveExampleController { 2 | @AuraEnabled(cacheable=true) 3 | public static Contact[] getContacts(String keyword) { 4 | Contact[] contacts = [ 5 | SELECT Id, Name, Email, Phone, Title 6 | FROM Contact 7 | WHERE Name LIKE :('%' + keyword + '%') 8 | WITH SECURITY_ENFORCED 9 | LIMIT 10 10 | ]; 11 | return contacts; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /force-app/main/default/classes/DirectiveExampleController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"], 3 | "overrides": [ 4 | { 5 | "files": ["*.test.js"], 6 | "rules": { 7 | "@lwc/lwc/no-unexpected-wire-adapter-usages": "off" 8 | }, 9 | "env": { 10 | "node": true 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparator/__tests__/directiveComparator.test.js: -------------------------------------------------------------------------------- 1 | import { comparator, STRING_VALUE, NUMBER_VALUE } from "c/directiveComparator"; 2 | 3 | describe("c-directive-comparator", () => { 4 | afterEach(() => {}); 5 | 6 | const ACCOUNT1 = { 7 | Id: "acct1", 8 | Name: "Acme Corporation" 9 | }; 10 | 11 | const CONTACTS = [ 12 | { 13 | Id: "cont1", 14 | Name: "Amy Taylor", 15 | Title: "VP of Engineering", 16 | Account: ACCOUNT1 17 | }, 18 | { 19 | Id: "cont2", 20 | Name: "Michael Jones", 21 | Title: "VP of Sales", 22 | Account: ACCOUNT1 23 | }, 24 | { Id: "cont3", Name: "Jennifer Wu", Title: "CEO", Account: ACCOUNT1 }, 25 | { 26 | Id: "cont4", 27 | Name: "John Doe", 28 | Title: "VP of Engineering", 29 | Account: ACCOUNT1 30 | }, 31 | { 32 | Id: "cont5", 33 | Name: "Jane Doe", 34 | Title: "VP of Sales", 35 | Account: ACCOUNT1 36 | } 37 | ]; 38 | 39 | it("check comparator works correctly", () => { 40 | const context = { 41 | contactId: "cont1", 42 | keyword: "or", 43 | account: ACCOUNT1, 44 | contacts: CONTACTS, 45 | get filteredContacts() { 46 | return this.contacts.filter((contact) => 47 | contact.Name.toLowerCase().includes(this.keyword.toLowerCase()) 48 | ); 49 | } 50 | }; 51 | const $ = comparator(context, { 52 | contactId: STRING_VALUE, 53 | keyword: STRING_VALUE, 54 | account: { 55 | Id: STRING_VALUE 56 | }, 57 | contacts: [ 58 | { 59 | Id: STRING_VALUE, 60 | Account: { 61 | Id: STRING_VALUE 62 | } 63 | } 64 | ], 65 | filteredContacts: [] 66 | }); 67 | expect($.contacts.length.gt.zero).toBe(true); 68 | expect($.filteredContacts.length.gt.zero).toBe(true); 69 | expect($.account.Name.includes.$keyword).toBe(true); 70 | for (const [index, contact] of [...$.contacts].entries()) { 71 | expect( 72 | index === 0 73 | ? contact.$.Id.equals.$contactId 74 | : contact.$.Id.not.equals.$contactId 75 | ).toBe(true); 76 | expect(contact.$.Account.Id.equals.$account.Id).toBe(true); 77 | } 78 | // mutate context property value 79 | context.contactId = "cont2"; 80 | context.keyword = "Ad"; 81 | expect($.account.Name.not.includes.$keyword).toBe(true); 82 | expect($.filteredContacts.isEmpty).toBe(true); 83 | for (const [index, contact] of [...$.contacts].entries()) { 84 | expect( 85 | index === 1 86 | ? contact.$.Id.equals.$contactId 87 | : contact.$.Id.not.equals.$contactId 88 | ).toBe(true); 89 | } 90 | }); 91 | 92 | it("should accept constant definition", () => { 93 | const context = { 94 | type: "03. Competitor", 95 | limit: 100 96 | }; 97 | const $ = comparator( 98 | context, 99 | { 100 | type: [ 101 | ["customer", "01. Customer"], 102 | ["partner", "02. Partner"], 103 | ["competitor", "03. Competitor"] 104 | ], 105 | limit: NUMBER_VALUE 106 | }, 107 | { 108 | constants: { maxLimit: 100 } 109 | } 110 | ); 111 | expect($.type.equals.competitor).toBe(true); 112 | expect($.limit.lte.maxLimit).toBe(true); 113 | }); 114 | 115 | it("should do iterate with non-object array", () => { 116 | const context = { 117 | selected: "orange", 118 | fruits: ["apple", "orange", "melon", "banana"], 119 | num: 1, 120 | nums: [0, 1, 2, 3], 121 | }; 122 | 123 | const $ = comparator(context, { 124 | selected: STRING_VALUE, 125 | fruits: [STRING_VALUE], 126 | num: NUMBER_VALUE, 127 | nums: [NUMBER_VALUE], 128 | }); 129 | 130 | expect($.fruits.length.gt.zero).toBe(true); 131 | for (const [index, fruit] of [...$.fruits].entries()) { 132 | expect( 133 | index === 1 134 | ? fruit.$.equals.$selected 135 | : fruit.$.not.equals.$selected 136 | ).toBe(true); 137 | expect(fruit.$value).toBe(context.fruits[index]); 138 | } 139 | for (const [index, num] of [...$.nums].entries()) { 140 | expect( 141 | index === 1 142 | ? num.$.equals.$num 143 | : num.$.not.equals.$num 144 | ).toBe(true); 145 | expect(num.$value).toBe(context.nums[index]); 146 | } 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparator/directiveComparator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | export const STRING_VALUE = Symbol("STRING_VALUE"); 5 | 6 | export const NUMBER_VALUE = Symbol("NUMBER_VALUE"); 7 | 8 | export const BOOLEAN_VALUE = Symbol("BOOLEAN_VALUE"); 9 | 10 | export const ANY_VALUE = Symbol("ANY_VALUE"); 11 | 12 | const VALUE_SYMBOLS = new Set([ 13 | STRING_VALUE, 14 | NUMBER_VALUE, 15 | BOOLEAN_VALUE, 16 | ANY_VALUE 17 | ]); 18 | 19 | /** 20 | * 21 | */ 22 | const BINARY_COMPARISON_OPERATIONS = { 23 | equals: (v1, v2) => v1 === v2, 24 | is: (v1, v2) => v1 === v2, 25 | notEquals: (v1, v2) => v1 !== v2, 26 | isNot: (v1, v2) => v1 !== v2, 27 | greaterThan: (v1, v2) => v1 > v2, 28 | gt: (v1, v2) => v1 > v2, 29 | greaterThanOrEquals: (v1, v2) => v1 >= v2, 30 | gte: (v1, v2) => v1 >= v2, 31 | lessThan: (v1, v2) => v1 < v2, 32 | lt: (v1, v2) => v1 < v2, 33 | lessThanOrEquals: (v1, v2) => v1 <= v2, 34 | lte: (v1, v2) => v1 <= v2, 35 | startsWith: (v1, v2) => typeof v1 === "string" && v1.startsWith(v2), 36 | endsWith: (v1, v2) => typeof v1 === "string" && v1.endsWith(v2), 37 | includes: (v1, v2) => typeof v1 === "string" && v1.includes(v2) 38 | }; 39 | 40 | const NEGATABLE_BINARY_OPERATIONS = new Set([ 41 | "equals", 42 | "greaterThan", 43 | "gt", 44 | "greaterThanOrEqual", 45 | "gte", 46 | "lessThan", 47 | "lt", 48 | "lessThanOrEqual", 49 | "lte", 50 | "startsWith", 51 | "endsWith", 52 | "includes" 53 | ]); 54 | 55 | const UNARY_COMPARISON_OPERATIONS = { 56 | isTruthy: (v) => !!v, 57 | isFalsy: (v) => !v, 58 | isNull: (v) => v === null, 59 | isNotNull: (v) => v !== null, 60 | isUndefined: (v) => typeof v === "undefined", 61 | isNotUndefined: (v) => typeof v !== "undefined", 62 | isNullish: (v) => v == null, 63 | isNotNullish: (v) => v != null, 64 | isEmpty: (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0), 65 | isNotEmpty: (v) => 66 | v != null && v !== "" && (!Array.isArray(v) || v.length > 0) 67 | }; 68 | 69 | /** 70 | * 71 | */ 72 | const DEFAULT_COMPARISON_CONSTANTS = { 73 | zero: 0, 74 | one: 1, 75 | true: true, 76 | false: false, 77 | null: null, 78 | undefined: undefined 79 | }; 80 | 81 | function isObjectTypeDefinition(typeDef) { 82 | return ( 83 | typeof typeDef === "object" && typeDef !== null && !Array.isArray(typeDef) 84 | ); 85 | } 86 | 87 | function isArrayTypeDefinition(typeDef) { 88 | return ( 89 | Array.isArray(typeDef) && 90 | (typeDef.length === 0 || 91 | (typeDef.length === 1 && 92 | (isObjectTypeDefinition(typeDef[0]) || VALUE_SYMBOLS.has(typeDef[0])))) 93 | ); 94 | } 95 | 96 | function estimateTypeDef(value) { 97 | if (value == null) { 98 | return ANY_VALUE; 99 | } 100 | if (Array.isArray(value)) { 101 | const v = value[0]; 102 | return [v != null ? estimateTypeDef(v) : ANY_VALUE]; 103 | } 104 | const vtype = typeof value; 105 | switch (vtype) { 106 | case "object": { 107 | const objType = {}; 108 | // eslint-disable-next-line guard-for-in 109 | for (const property in value) { 110 | try { 111 | const v = value[property]; 112 | if (typeof v !== "function") { 113 | objType[property] = estimateTypeDef(v); 114 | } 115 | } catch (e) { 116 | console.error(e); 117 | } 118 | } 119 | return objType; 120 | } 121 | case "string": 122 | return STRING_VALUE; 123 | case "number": 124 | return NUMBER_VALUE; 125 | case "boolean": 126 | return BOOLEAN_VALUE; 127 | default: 128 | return ANY_VALUE; 129 | } 130 | } 131 | 132 | /** 133 | * 134 | */ 135 | function toArray(value) { 136 | if (value == null) { 137 | return value; 138 | } 139 | if ( 140 | typeof value === "object" && 141 | typeof value[Symbol.iterator] === "function" 142 | ) { 143 | return Array.from(value); 144 | } 145 | return [value]; 146 | } 147 | 148 | function toObjectValue(value) { 149 | const t = typeof value; 150 | if (value == null || t === "string" || t === "number" || t === "boolean") { 151 | const objValue = { 152 | valueOf() { 153 | return value; 154 | }, 155 | get $value() { 156 | return value; 157 | } 158 | }; 159 | if (t === "string") { 160 | Object.defineProperty(objValue, "length", { 161 | get() { 162 | return value.length; 163 | } 164 | }); 165 | } 166 | return objValue; 167 | } 168 | return value; 169 | } 170 | 171 | function toNameValuePairs(values) { 172 | const nameValuePairs = []; 173 | for (const val of values) { 174 | if (typeof val === "object" && val != null && !Array.isArray(val)) { 175 | for (const [name, value] of Object.entries(val)) { 176 | nameValuePairs.push([name, value]); 177 | } 178 | } else { 179 | const [name, value] = toArray(val); 180 | nameValuePairs.push([name, value ?? name]); 181 | } 182 | } 183 | return nameValuePairs; 184 | } 185 | 186 | /** 187 | * 188 | */ 189 | function createMemo(getKeyValue) { 190 | let lastKeyValue; 191 | let memoResult; 192 | return (fn) => { 193 | const currKeyValue = getKeyValue(); 194 | if (lastKeyValue == null || currKeyValue !== lastKeyValue) { 195 | memoResult = fn(currKeyValue); 196 | lastKeyValue = currKeyValue; 197 | } 198 | return memoResult; 199 | }; 200 | } 201 | 202 | /** 203 | * 204 | */ 205 | function createComparator(context, contextType, options = {}) { 206 | console.log("createComparator", options.contextPath); 207 | const comp = { 208 | get $value() { 209 | return context; 210 | } 211 | }; 212 | const estimatedContextType = options.estimateProps 213 | ? estimateTypeDef(context) 214 | : ANY_VALUE; 215 | contextType = contextType === ANY_VALUE ? estimatedContextType : contextType; 216 | if (isObjectTypeDefinition(contextType)) { 217 | const propTypes = { 218 | ...estimatedContextType, 219 | ...contextType 220 | }; 221 | definePropertyComparators(comp, context, propTypes, options); 222 | } 223 | if (contextType === STRING_VALUE) { 224 | definePropertyComparators(comp, context, { length: NUMBER_VALUE }, options); 225 | } 226 | defineBinaryComparisonOperations(comp, contextType, options); 227 | defineUnaryComparisonOperations(comp, options); 228 | return comp; 229 | } 230 | 231 | /** 232 | * 233 | */ 234 | function createIteratorComparator(items, itemType, options) { 235 | console.log("createIteratorComparator", options.contextPath); 236 | const comp = createComparator( 237 | items, 238 | { length: NUMBER_VALUE }, 239 | { ...options, estimateProps: false } 240 | ); 241 | const iterItems = toArray(items)?.map((item, index) => { 242 | const itemOptions = { 243 | ...options, 244 | contextProperty: `${index}`, 245 | contextPath: `${options.contextPath}[${index}]`, 246 | estimateProps: true 247 | }; 248 | const itemComp = createComparator( 249 | item, 250 | itemType, 251 | itemOptions 252 | ); 253 | return { 254 | ...toObjectValue(item), 255 | [options.comparatorNamespace]: itemComp, 256 | }; 257 | }); 258 | comp[Symbol.iterator] = () => { 259 | let index = 0; 260 | return { 261 | next() { 262 | const done = index >= (iterItems?.length ?? 0); 263 | const value = done ? undefined : iterItems?.[index++]; 264 | return { done, value }; 265 | } 266 | }; 267 | }; 268 | return comp; 269 | } 270 | 271 | /** 272 | * 273 | */ 274 | function createComparisonOperand(satisfies, valueDef, options) { 275 | const valueDefs = toArray(valueDef ?? []); 276 | const additionalValues = valueDefs.filter( 277 | (value) => !VALUE_SYMBOLS.has(value) 278 | ); 279 | const nameValuePairs = toNameValuePairs([ 280 | ...additionalValues, 281 | ...toArray(options.constants ?? []), 282 | DEFAULT_COMPARISON_CONSTANTS 283 | ]); 284 | const vals = {}; 285 | for (const [name, value] of nameValuePairs) { 286 | if (name in vals) { 287 | continue; 288 | } 289 | Object.defineProperty(vals, name, { 290 | get() { 291 | return satisfies(value, name); 292 | } 293 | }); 294 | } 295 | const { rootContext, rootContextType, rootOptions } = options; 296 | defineContextOperandValues(vals, satisfies, rootContext, rootContextType, { 297 | ...rootOptions, 298 | propertyPrefix: "$" 299 | }); 300 | return vals; 301 | } 302 | 303 | /** 304 | * 305 | */ 306 | function definePropertyComparators(obj, context, propTypes, options) { 307 | for (const [property, propType] of Object.entries(propTypes)) { 308 | if (property in obj) { 309 | continue; 310 | } 311 | const propOptions = { 312 | ...options, 313 | contextProperty: `${property}`, 314 | contextPath: `${options.contextPath}.${property}`, 315 | estimateProps: true 316 | }; 317 | const memo = createMemo(() => context?.[property]); 318 | let createPropertyComparator; 319 | if (isArrayTypeDefinition(propType)) { 320 | const itemType = propType?.[0] ?? ANY_VALUE; 321 | createPropertyComparator = (value) => 322 | createIteratorComparator(value, itemType, propOptions); 323 | } else { 324 | createPropertyComparator = (value) => 325 | createComparator(value, propType, propOptions); 326 | } 327 | Object.defineProperty(obj, property, { 328 | get() { 329 | return memo(createPropertyComparator); 330 | } 331 | }); 332 | } 333 | } 334 | 335 | /** 336 | * 337 | */ 338 | function defineBinaryComparisonOperations(comp, valueDef, options) { 339 | const notOpr = {}; 340 | for (const [operation, compareValue] of Object.entries( 341 | BINARY_COMPARISON_OPERATIONS 342 | )) { 343 | if (operation in comp) { 344 | continue; 345 | } 346 | const createChecker = 347 | (compare, { operationPath }) => 348 | (rvalue, rpath) => { 349 | const lvalue = comp.$value; 350 | const ret = compare(lvalue, rvalue); 351 | console.log( 352 | `compare: ${options.contextPath}.${operationPath}.${rpath} => ${ret}`, 353 | ", left = ", 354 | lvalue, 355 | ", right = ", 356 | rvalue 357 | ); 358 | return ret; 359 | }; 360 | const satisfies = createChecker(compareValue, { operationPath: operation }); 361 | comp[operation] = createComparisonOperand(satisfies, valueDef, options); 362 | if (NEGATABLE_BINARY_OPERATIONS.has(operation)) { 363 | const notSatisfies = createChecker((v1, v2) => !compareValue(v1, v2), { 364 | operationPath: `not.${operation}` 365 | }); 366 | notOpr[operation] = createComparisonOperand( 367 | notSatisfies, 368 | valueDef, 369 | options 370 | ); 371 | } 372 | } 373 | comp.not = notOpr; 374 | } 375 | 376 | /** 377 | * 378 | */ 379 | function defineUnaryComparisonOperations(comp, options) { 380 | for (const [operation, compare] of Object.entries( 381 | UNARY_COMPARISON_OPERATIONS 382 | )) { 383 | if (operation in comp) { 384 | continue; 385 | } 386 | Object.defineProperty(comp, operation, { 387 | get() { 388 | const value = comp.$value; 389 | const ret = compare(value); 390 | console.log( 391 | `compare: ${options.contextPath}.${operation} => ${ret}`, 392 | ", value = ", 393 | value 394 | ); 395 | return ret; 396 | } 397 | }); 398 | } 399 | } 400 | 401 | /** 402 | * 403 | */ 404 | function defineContextOperandValues( 405 | operand, 406 | satisfies, 407 | context, 408 | contextType, 409 | options 410 | ) { 411 | const getContext = typeof context === "function" ? context : () => context; 412 | const estimatedContextType = options.estimateProps 413 | ? estimateTypeDef(getContext()) 414 | : {}; 415 | contextType = 416 | contextType == null || contextType === ANY_VALUE 417 | ? estimatedContextType 418 | : contextType; 419 | const objType = { 420 | ...estimatedContextType, 421 | ...contextType 422 | }; 423 | for (const [property, propType] of Object.entries(objType)) { 424 | const prefixedProperty = `${options.propertyPrefix ?? ""}${property}`; 425 | const propOptions = { 426 | ...options, 427 | estimateProps: true, 428 | propertyPrefix: "" 429 | }; 430 | if (isObjectTypeDefinition(propType)) { 431 | const objOperand = {}; 432 | defineContextOperandValues( 433 | objOperand, 434 | satisfies, 435 | () => getContext()?.[property], 436 | propType, 437 | propOptions 438 | ); 439 | operand[prefixedProperty] = objOperand; 440 | } else if (isArrayTypeDefinition(propType)) { 441 | // TODO 442 | } else { 443 | const propertyPath = options.contextPath 444 | ? `${options.contextPath}.${prefixedProperty}` 445 | : prefixedProperty; 446 | Object.defineProperty(operand, prefixedProperty, { 447 | get() { 448 | const value = getContext()?.[property]; 449 | return satisfies(value, propertyPath); 450 | } 451 | }); 452 | } 453 | } 454 | } 455 | 456 | /** 457 | * 458 | */ 459 | export function comparator(context, contextType = {}, options = {}) { 460 | const contextProperty = options.contextProperty ?? "$"; 461 | const contextPath = options.contextPath ?? "$"; 462 | const comparatorNamespace = options.comparatorNamespace ?? "$"; 463 | const estimateProps = options.estimateProps ?? false; 464 | options = { 465 | ...options, 466 | rootOptions: options, 467 | rootContext: context, 468 | rootContextType: contextType, 469 | contextProperty, 470 | contextPath, 471 | comparatorNamespace, 472 | estimateProps 473 | }; 474 | return createComparator(context, contextType, options); 475 | } 476 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparator/directiveComparator.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorContactSearchExample/__tests__/directiveComparatorContactSearchExample.test.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'lwc'; 2 | import DirectiveComparatorContactSearchExample from 'c/directiveComparatorContactSearchExample'; 3 | 4 | describe('c-directive-comparator-contact-search-example', () => { 5 | afterEach(() => { 6 | // The jsdom instance is shared across test cases in a single file so reset the DOM 7 | while (document.body.firstChild) { 8 | document.body.removeChild(document.body.firstChild); 9 | } 10 | }); 11 | 12 | it('TODO: test case generated by CLI command, please fill in test logic', () => { 13 | // Arrange 14 | const element = createElement('c-directive-comparator-contact-search-example', { 15 | is: DirectiveComparatorContactSearchExample 16 | }); 17 | 18 | // Act 19 | document.body.appendChild(element); 20 | 21 | // Assert 22 | // const div = element.shadowRoot.querySelector('div'); 23 | expect(1).toBe(1); 24 | }); 25 | }); -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorContactSearchExample/directiveComparatorContactSearchExample.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorContactSearchExample/directiveComparatorContactSearchExample.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, wire } from "lwc"; 2 | import { comparator, STRING_VALUE } from "c/directiveComparator"; 3 | import getContacts from "@salesforce/apex/DirectiveExampleController.getContacts"; 4 | 5 | export default class DirectiveComparatorContactSearchExample extends LightningElement { 6 | contactId = null; 7 | 8 | keywordInput = ""; 9 | 10 | keyword = ""; 11 | 12 | @wire(getContacts, { keyword: "$keyword" }) 13 | contacts; 14 | 15 | $ = comparator( 16 | this, 17 | { 18 | contactId: STRING_VALUE, 19 | keywordInput: STRING_VALUE, 20 | contacts: { 21 | data: [] 22 | } 23 | }, 24 | { 25 | constants: { 26 | five: 5 27 | } 28 | } 29 | ); 30 | 31 | handleChangeKeywordInput(e) { 32 | this.keywordInput = e.target.value; 33 | } 34 | 35 | handleKeydownKeywordInput(e) { 36 | if (e.key === "Enter" && !e.isComposing && e.keyCode !== 229) { 37 | if (this.keywordInput !== this.keyword) { 38 | // this.contacts = null; 39 | this.keyword = this.keywordInput; 40 | } 41 | } 42 | } 43 | 44 | handleClickContact(event) { 45 | this.contactId = event.target.dataset.id; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorContactSearchExample/directiveComparatorContactSearchExample.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | false 5 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorExample/__tests__/directiveComparatorExample.test.js: -------------------------------------------------------------------------------- 1 | import { createElement } from "lwc"; 2 | import DirectiveComparatorExample from "c/directiveComparatorExample"; 3 | 4 | describe("c-directive-comparator-example", () => { 5 | afterEach(() => { 6 | // The jsdom instance is shared across test cases in a single file so reset the DOM 7 | while (document.body.firstChild) { 8 | document.body.removeChild(document.body.firstChild); 9 | } 10 | }); 11 | 12 | it("TODO: test case generated by CLI command, please fill in test logic", () => { 13 | // Arrange 14 | const element = createElement("c-directive-comparator-example", { 15 | is: DirectiveComparatorExample 16 | }); 17 | 18 | // Act 19 | document.body.appendChild(element); 20 | 21 | // Assert 22 | // const div = element.shadowRoot.querySelector('div'); 23 | expect(1).toBe(1); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorExample/directiveComparatorExample.html: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorExample/directiveComparatorExample.js: -------------------------------------------------------------------------------- 1 | import { LightningElement } from "lwc"; 2 | 3 | export default class DirectiveComparatorExample extends LightningElement {} 4 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorExample/directiveComparatorExample.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | true 5 | 6 | lightning__Tab 7 | lightning__RecordPage 8 | lightning__AppPage 9 | lightning__HomePage 10 | 11 | 12 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorIterationExample/__tests__/directiveComparatorIterationExample.test.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'lwc'; 2 | import DirectiveComparatorIterationExample from 'c/directiveComparatorIterationExample'; 3 | 4 | describe('c-directive-comparator-iteration-example', () => { 5 | afterEach(() => { 6 | // The jsdom instance is shared across test cases in a single file so reset the DOM 7 | while (document.body.firstChild) { 8 | document.body.removeChild(document.body.firstChild); 9 | } 10 | }); 11 | 12 | it('TODO: test case generated by CLI command, please fill in test logic', () => { 13 | // Arrange 14 | const element = createElement('c-directive-comparator-iteration-example', { 15 | is: DirectiveComparatorIterationExample 16 | }); 17 | 18 | // Act 19 | document.body.appendChild(element); 20 | 21 | // Assert 22 | // const div = element.shadowRoot.querySelector('div'); 23 | expect(1).toBe(1); 24 | }); 25 | }); -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorIterationExample/directiveComparatorIterationExample.html: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorIterationExample/directiveComparatorIterationExample.js: -------------------------------------------------------------------------------- 1 | import { LightningElement } from "lwc"; 2 | import { comparator, NUMBER_VALUE } from "c/directiveComparator"; 3 | 4 | export default class DirectiveComparatorIterationExample extends LightningElement { 5 | customerId = 1; 6 | 7 | customers = [ 8 | { id: 1, fullName: "John Doe", rank: "gold" }, 9 | { id: 2, fullName: "Amy Taylor", rank: "silver" }, 10 | { id: 3, fullName: "Michael Jones", rank: "bronze" }, 11 | { id: 4, fullName: "Jane Doe", rank: "silver" } 12 | ]; 13 | 14 | $ = comparator(this, { 15 | customerId: NUMBER_VALUE, 16 | customers: [ 17 | { 18 | id: NUMBER_VALUE, 19 | rank: ["gold", "silver", "bronze"] 20 | } 21 | ] 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorIterationExample/directiveComparatorIterationExample.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | false 5 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorSimpleExample/__tests__/directiveComparatorSimpleExample.test.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'lwc'; 2 | import DirectiveComparatorSimpleExample from 'c/directiveComparatorSimpleExample'; 3 | 4 | describe('c-directive-comparator-simple-example', () => { 5 | afterEach(() => { 6 | // The jsdom instance is shared across test cases in a single file so reset the DOM 7 | while (document.body.firstChild) { 8 | document.body.removeChild(document.body.firstChild); 9 | } 10 | }); 11 | 12 | it('TODO: test case generated by CLI command, please fill in test logic', () => { 13 | // Arrange 14 | const element = createElement('c-directive-comparator-simple-example', { 15 | is: DirectiveComparatorSimpleExample 16 | }); 17 | 18 | // Act 19 | document.body.appendChild(element); 20 | 21 | // Assert 22 | // const div = element.shadowRoot.querySelector('div'); 23 | expect(1).toBe(1); 24 | }); 25 | }); -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorSimpleExample/directiveComparatorSimpleExample.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorSimpleExample/directiveComparatorSimpleExample.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from "lwc"; 2 | import { comparator } from "c/directiveComparator"; 3 | 4 | export default class DirectiveComparatorSimpleExample extends LightningElement { 5 | @api 6 | rank; 7 | 8 | @api 9 | fullName; 10 | 11 | $ = comparator(this, { 12 | rank: ["gold", "silver", "bronze"] 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/directiveComparatorSimpleExample/directiveComparatorSimpleExample.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 56.0 4 | false 5 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/Directive_Example.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | directiveComparatorExample 5 | Custom49: CD/DVD 6 | 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { jestConfig } = require("@salesforce/sfdx-lwc-jest/config"); 2 | 3 | module.exports = { 4 | ...jestConfig, 5 | modulePathIgnorePatterns: ["/.localdevserver"] 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwc-directive-comparator", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "LWC Directive Comparator", 6 | "scripts": { 7 | "lint": "eslint **/{aura,lwc}/**", 8 | "test": "npm run test:unit", 9 | "test:unit": "sfdx-lwc-jest", 10 | "test:unit:watch": "sfdx-lwc-jest --watch", 11 | "test:unit:debug": "sfdx-lwc-jest --debug", 12 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 13 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 14 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 15 | "postinstall": "husky install", 16 | "precommit": "lint-staged" 17 | }, 18 | "devDependencies": { 19 | "@lwc/eslint-plugin-lwc": "^1.1.2", 20 | "@prettier/plugin-xml": "^2.0.1", 21 | "@salesforce/eslint-config-lwc": "^3.2.3", 22 | "@salesforce/eslint-plugin-aura": "^2.0.0", 23 | "@salesforce/eslint-plugin-lightning": "^1.0.0", 24 | "@salesforce/sfdx-lwc-jest": "^1.1.0", 25 | "eslint": "^8.11.0", 26 | "eslint-plugin-import": "^2.25.4", 27 | "eslint-plugin-jest": "^26.1.2", 28 | "husky": "^7.0.4", 29 | "lint-staged": "^12.3.7", 30 | "prettier": "^2.6.0", 31 | "prettier-plugin-apex": "^1.10.0" 32 | }, 33 | "lint-staged": { 34 | "**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}": [ 35 | "prettier --write" 36 | ], 37 | "**/{aura,lwc}/**": [ 38 | "eslint" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "name": "lwc-directive-comparator", 9 | "namespace": "", 10 | "sfdcLoginUrl": "https://login.salesforce.com", 11 | "sourceApiVersion": "56.0" 12 | } 13 | --------------------------------------------------------------------------------