Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Monaco as AML editor, try 2 #336

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

Use Monaco as AML editor, try 2 #336

wants to merge 3 commits into from

Conversation

loicknuchel
Copy link
Contributor

@loicknuchel loicknuchel commented Nov 29, 2024

Summary by CodeRabbit

  • New Features

    • Introduced a new custom HTML element, IntlDate, for displaying localized date information.
    • Added a new custom HTML element, ElmMonaco, for enhanced code editing capabilities.
    • Replaced AzEditor with a new custom element, ElmTextarea, featuring a character counter.
  • Refactor

    • Enhanced the AmlSidebar component for improved user input handling and error/warning display.
  • Bug Fixes

    • Improved error handling and clarity in the AmlSidebar component's message processing.
  • Chores

    • Updated component initialization and import statements in the main index file.

Copy link

coderabbitai bot commented Nov 29, 2024

Walkthrough

The changes in this pull request involve significant updates to several components within the frontend codebase. The AmlSidebar.elm file has been modified to improve imports, function signatures, and the view structure, enhancing user interaction handling. The elm-monaco.ts and elm-textarea.ts files have undergone refactoring to rename and redefine custom HTML elements, updating their logging and lifecycle methods. A new custom element, IntlDate, has been introduced to manage localized date display. Additionally, the index.ts file has been updated to streamline component initialization.

Changes

File Path Change Summary
frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm Updated imports, modified view, viewErrors, viewWarnings, and update functions for better structure and error handling.
frontend/ts-src/components/elm-monaco.ts Added a new custom HTML element ElmMonaco, with updated lifecycle methods and logging.
frontend/ts-src/components/elm-textarea.ts Introduced a new custom HTML element ElmTextarea, replacing AzEditor, with enhanced logging and event handling.
frontend/ts-src/components/intl-date.ts Introduced a new custom HTML element IntlDate, with methods for localization and attribute handling.
frontend/ts-src/index.ts Updated imports and initialization for components, activating previously commented-out imports.

Poem

🐰 In the land of code where bunnies play,
New elements bloom, brightening the day.
ElmMonaco hops, with a textarea too,
IntlDate joins in, with a date just for you!
With each little change, our project will grow,
Hooray for the updates, let the good times flow! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (11)
frontend/ts-src/components/elm-monaco.ts (4)

Line range hint 1-48: Critical: The entire file is commented out

The entire file content is wrapped in block comments /* */, which would prevent any functionality from working. This appears to be unintentional and would break the Monaco editor integration.

Remove the block comments:

-/*import * as monaco from "monaco-editor";
+import * as monaco from "monaco-editor";

// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md

export function loadElmMonaco(): void {
    customElements.define('elm-monaco', ElmMonaco)
}
...
-*/

Line range hint 9-24: Consider making the editor height configurable

The editor height is hardcoded to 500px in the constructor. Consider making this configurable through attributes or CSS variables for better flexibility.

 this.container = document.createElement('div')
 this.container.style.width = '100%'
-this.container.style.height = '500px'
+this.container.style.height = 'var(--editor-height, 500px)'

Line range hint 25-33: Improve error handling and initial value management

The editor initialization could be improved in several ways:

  1. The initial value is hardcoded to "Hello World"
  2. There's no error handling for editor creation
  3. Exposing the editor instance to window is potentially dangerous
-console.log('create monaco editor')
+const initialValue = this.getAttribute('value') || ''
 this.editor = monaco.editor.create(this.container, {
   automaticLayout: true,
   language: 'html',
-  value: `<div>Hello World</div>`,
+  value: initialValue,
 });
-window.editor = this.editor
-console.log('monaco editor created')
+if (!this.editor) {
+  console.error('Failed to create Monaco editor')
+  return
+}

Line range hint 9-48: Missing Elm integration mechanism

The component is intended to integrate with Elm, but there's no visible mechanism for communicating changes back to Elm. Consider adding custom events or implementing the necessary port communication.

Consider adding:

  1. Value change events
  2. Focus/blur events
  3. Error handling events

Example implementation:

// Add to the editor creation options
this.editor = monaco.editor.create(this.container, {
  // ... existing options
  onChange: (e) => {
    this.dispatchEvent(new CustomEvent('editorChange', {
      detail: { value: this.editor.getValue() }
    }))
  }
});
frontend/ts-src/components/intl-date.ts (4)

Line range hint 34-39: Enhance input validation patterns

The current regex patterns could be more robust:

  • lang pattern ([a-z]{2}-[A-Z]{2}) doesn't support all valid locale formats (e.g., en-IN, zh-Hans-CN)
  • Year and month patterns don't enforce reasonable ranges

Consider this improved validation:

-        } else if (lang.match(/^[a-z]{2}-[A-Z]{2}$/) === null) {
+        } else if (!Intl.DateTimeFormat.supportedLocalesOf([lang]).length) {
             this.textContent = 'invalid value for "lang" parameter'
-        } else if (yearStr.match(/^\d+$/) === null) {
+        } else if (!/^\d{1,4}$/.test(yearStr)) {
             this.textContent = 'invalid value for "year" parameter'
-        } else if (monthStr.match(/^\d+$/) === null) {
+        } else if (!/^([0-9]|1[0-1])$/.test(monthStr)) {
             this.textContent = 'invalid value for "month" parameter'

Line range hint 40-42: Fix month parameter handling and add type safety

The month value is passed directly to Date constructor without adjustment, which could lead to incorrect dates since JavaScript months are 0-based.

Apply this fix:

         } else {
-            this.textContent = localizeDate(lang, parseInt(yearStr), parseInt(monthStr))
+            const year = parseInt(yearStr, 10)
+            const month = parseInt(monthStr, 10)
+            if (isNaN(year) || isNaN(month)) {
+                this.textContent = 'invalid numeric values'
+                return
+            }
+            this.textContent = localizeDate(lang, year, month - 1) // Adjust for 0-based months
         }

Line range hint 33-42: Improve error message clarity

The error messages could be more informative to help users understand the expected format.

Consider enhancing the error messages:

-            this.textContent = 'invalid value for "lang" parameter'
+            this.textContent = 'Invalid locale format. Example: en-US, fr-FR'
-            this.textContent = 'invalid value for "year" parameter'
+            this.textContent = 'Year must be a 1-4 digit number'
-            this.textContent = 'invalid value for "month" parameter'
+            this.textContent = 'Month must be between 0 and 11'

Line range hint 49-60: Add error handling and optimize performance

The localizeDate function could benefit from several improvements:

  1. Error handling for invalid dates
  2. Support for different calendar systems
  3. Performance optimization through DateTimeFormat caching

Consider this enhanced implementation:

+const formatters = new Map<string, Intl.DateTimeFormat>();
+
 function localizeDate(lang: string, year: number, month: number): string {
-    const dateTimeFormat = new Intl.DateTimeFormat(lang, {
-        weekday: 'long',
-        year: 'numeric',
-        month: 'long',
-        day: 'numeric'
-    })
+    // Get or create formatter
+    let formatter = formatters.get(lang);
+    if (!formatter) {
+        formatter = new Intl.DateTimeFormat(lang, {
+            weekday: 'long',
+            year: 'numeric',
+            month: 'long',
+            day: 'numeric',
+            calendar: 'gregory' // Ensure consistent calendar system
+        });
+        formatters.set(lang, formatter);
+    }
 
-    return dateTimeFormat.format(new Date(year, month))
+    // Create and validate date
+    const date = new Date(year, month);
+    if (isNaN(date.getTime())) {
+        return 'Invalid date';
+    }
+
+    return formatter.format(date);
 }
frontend/ts-src/components/elm-textarea.ts (3)

42-45: Consider enhancing initialization robustness and logging security

While the initialization logic works, consider these improvements:

  1. Add type checking for the value attribute
  2. Remove or conditionally enable value logging in production
  3. Consider handling null shadowRoot case explicitly
 connectedCallback() {
-    console.log('ElmTextarea.connectedCallback')
-    const value = this.getAttribute('value') || ''
-    console.log('connectedCallback.value', value)
-    this.textarea.value = value
+    if (process.env.NODE_ENV !== 'production') {
+        console.log('ElmTextarea.connectedCallback')
+    }
+    const value = this.getAttribute('value')
+    if (this.textarea && value !== null) {
+        this.textarea.value = value
+    }
     this.updateCounter()
     this.textarea.addEventListener('input', this.updateCounter.bind(this))
 }

51-51: Consider conditional logging for production

The logging statement should be conditionally enabled only in development.

-    console.log('ElmTextarea.disconnectedCallback')
+    if (process.env.NODE_ENV !== 'production') {
+        console.log('ElmTextarea.disconnectedCallback')
+    }

61-61: Enhance attribute handling with type safety and secure logging

Consider improving the attribute handling:

  1. Add type safety for attribute names
  2. Remove or conditionally enable attribute value logging in production
  3. Consider sanitizing logged values
+    type ValidAttributes = 'value' | 'readonly' | 'disabled';
     attributeChangedCallback(name: string, oldValue: string, newValue: string) {
-        console.log('ElmTextarea.attributeChangedCallback', name, newValue)
+        if (process.env.NODE_ENV !== 'production') {
+            // Log attribute name but not value for security
+            console.log('ElmTextarea.attributeChangedCallback', name)
+        }
+        if (!this.constructor.observedAttributes.includes(name)) {
+            return;
+        }
         if (name === 'value') {
             this.textarea.value = newValue
             this.updateCounter()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 421392d and ce2ba52.

📒 Files selected for processing (5)
  • frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm (2 hunks)
  • frontend/ts-src/components/elm-monaco.ts (4 hunks)
  • frontend/ts-src/components/elm-textarea.ts (3 hunks)
  • frontend/ts-src/components/intl-date.ts (1 hunks)
  • frontend/ts-src/index.ts (2 hunks)
🔇 Additional comments (5)
frontend/ts-src/components/intl-date.ts (2)

Line range hint 1-3: LGTM: Custom element registration is correct

The loadIntlDate function properly registers the custom element using the standard Web Components API.


Line range hint 5-11: Verify component integration with Elm

The example comments show basic usage, but we should verify the actual integration with Elm templates.

✅ Verification successful

Let me gather more information about the Elm integration.


Component integration with Elm is properly implemented

The verification shows that:

  • The custom element is correctly loaded before Elm initialization in frontend/ts-src/index.ts
  • The component is actively used in Elm templates, specifically in AmlSidebar.elm with proper attribute binding
  • The implementation follows the example pattern from the Elm guide for custom elements
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usage of intl-date element in Elm templates
rg -t elm '<intl-date' 

# Check if the component is properly loaded before Elm initialization
rg -A 5 'loadIntlDate' frontend/ts-src/index.ts

Length of output: 626


Script:

#!/bin/bash
# Search for intl-date component registration and usage
rg -A 5 'class IntlDate' frontend/ts-src/components/intl-date.ts

# Check if there are any Elm view functions or decoders related to intl-date
rg -t elm 'IntlDate|intl-date' 

# Look for any Elm ports or flags related to date formatting
ast-grep --pattern 'port module $$$'

Length of output: 470

frontend/ts-src/components/elm-textarea.ts (2)

3-4: LGTM: Clean implementation of custom element registration

The function follows web components best practices and maintains a clear, single responsibility.


3-4: Verify component rename impact

The component has been renamed from az-editor to elm-textarea. Let's verify all usages have been updated.

✅ Verification successful

Let me check the actual content of these files to better understand the usage context.


Based on the files analyzed, I can now generate the final response:

Component rename is complete and consistent

The component rename from az-editor to elm-textarea has been properly implemented:

  • The new component is correctly defined in frontend/ts-src/components/elm-textarea.ts
  • It's properly imported and used in frontend/ts-src/index.ts
  • No remaining references to the old az-editor component name were found in the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to the old component name
echo "Checking for old component references..."
rg -l "az-editor"

# Search for usage of new component name to verify migration
echo "Checking new component usage..."
rg -l "elm-textarea"

Length of output: 348


Script:

#!/bin/bash
# Check content of files for context
echo "=== Content of frontend/ts-src/index.ts ==="
cat frontend/ts-src/index.ts

echo -e "\n=== Content of frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm ==="
cat frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm

Length of output: 47345

frontend/ts-src/index.ts (1)

79-81: Verify the status of Monaco editor integration

The commented import loadElmMonaco suggests that the Monaco editor integration is incomplete or in progress. This seems inconsistent with the PR title "Use Monaco as AML editor, try 2".

Comment on lines 46 to 48
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log('AmlEditor.attributeChangedCallback', name, newValue)
console.log('ElmMonaco.attributeChangedCallback', name, newValue)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement attribute change handling

The attributeChangedCallback only logs the changes but doesn't actually update the editor state. Consider implementing proper handling for the observed attributes.

 attributeChangedCallback(name: string, oldValue: string, newValue: string) {
-    console.log('ElmMonaco.attributeChangedCallback', name, newValue)
+    if (!this.editor) return
+    
+    switch (name) {
+        case 'value':
+            if (this.editor.getValue() !== newValue) {
+                this.editor.setValue(newValue)
+            }
+            break
+        case 'readonly':
+            this.editor.updateOptions({ readOnly: newValue !== null })
+            break
+        case 'disabled':
+            // Handle disabled state
+            break
+    }
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +5 to +6
export function loadElmMonaco(): void {
customElements.define('elm-monaco', ElmMonaco)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Update required in Elm code for custom element name

Found one remaining reference to aml-editor in frontend/src/Components/Molecules/Editor.elm that needs to be updated to use the new elm-monaco custom element name.

  • frontend/src/Components/Molecules/Editor.elm: Update the custom element name from aml-editor to elm-monaco in the editor component initialization.
🔗 Analysis chain

Verify custom element registration

The component registration looks correct, but we should verify that all references to the old aml-editor custom element have been updated across the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to the old custom element name
rg "aml-editor" --type html --type typescript --type elm

Length of output: 91


Script:

#!/bin/bash
# Search for any remaining references to the old custom element name using ripgrep
# Note: Using specific file extensions instead of file types
rg "aml-editor" -g "*.ts" -g "*.tsx" -g "*.html" -g "*.elm"

Length of output: 303

Comment on lines +83 to +85
loadIntlDate() // should be before the Elm init
loadElmTextarea() // should be before the Elm init
// loadElmMonaco() // should be before the Elm init
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider handling component initialization failures

While the initialization order is correctly documented with comments, there's no error handling for component initialization failures. This could lead to silent failures if either loadIntlDate or loadElmTextarea fails.

Consider wrapping the initialization in a try-catch block and adding error handling:

-loadIntlDate() // should be before the Elm init
-loadElmTextarea() // should be before the Elm init
+try {
+  loadIntlDate() // should be before the Elm init
+  loadElmTextarea() // should be before the Elm init
+  // loadElmMonaco() // should be before the Elm init
+} catch (error) {
+  console.error('Failed to initialize components:', error)
+  // Consider showing a user-friendly error message or fallback gracefully
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loadIntlDate() // should be before the Elm init
loadElmTextarea() // should be before the Elm init
// loadElmMonaco() // should be before the Elm init
try {
loadIntlDate() // should be before the Elm init
loadElmTextarea() // should be before the Elm init
// loadElmMonaco() // should be before the Elm init
} catch (error) {
console.error('Failed to initialize components:', error)
// Consider showing a user-friendly error message or fallback gracefully
}

[ -- , node "intl-date" [ attribute "lang" "fr-FR", attribute "year" (String.fromInt 2024), attribute "month" (String.fromInt 9) ] []
-- , node "aml-editor" [ value (contentStr source), onInput (AUpdateSource source.id >> AmlSidebarMsg), onBlur (ASourceUpdated source.id |> AmlSidebarMsg) ] []
Editor.basic "source-editor" (Source.contentStr source) (AUpdateSource source.id >> AmlSidebarMsg) (ASourceUpdated source.id |> AmlSidebarMsg) """Write your schema using AML syntax
[ div [] [ node "intl-date" [ attribute "lang" "fr-FR", attribute "year" (String.fromInt 2024), attribute "month" (String.fromInt 9) ] [] ]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unintended intl-date component

The intl-date component on line 307 appears to be for testing purposes with hardcoded values for lang, year, and month. If this component is not intended for production, consider removing it to avoid unintended side effects.

Apply this diff to remove the unintended component:

-            , div [] [ node "intl-date" [ attribute "lang" "fr-FR", attribute "year" (String.fromInt 2024), attribute "month" (String.fromInt 9) ] [] ]

Comment on lines +308 to +309
, div [] [ node "elm-textarea" [ value (Source.contentStr source), onInput (AUpdateSource source.id >> AmlSidebarMsg), onBlur (ASourceUpdated source.id |> AmlSidebarMsg) ] [] ]
, Editor.basic "source-editor" (Source.contentStr source) (AUpdateSource source.id >> AmlSidebarMsg) (ASourceUpdated source.id |> AmlSidebarMsg) """Write your schema using AML syntax
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid duplicate editor components

Both elm-textarea and Editor.basic are rendering editor interfaces. If elm-textarea is intended to replace Editor.basic, consider removing Editor.basic to prevent duplication and potential inconsistencies.

Apply this diff to remove the redundant editor component:

-            , Editor.basic "source-editor" (Source.contentStr source) (AUpdateSource source.id >> AmlSidebarMsg) (ASourceUpdated source.id |> AmlSidebarMsg) """Write your schema using AML syntax

Ex:

users
  id uuid pk
  first_name varchar(128)
  last_name varchar(128)
  email varchar(128) nullable

credentials | used to authenticate users
  user_id pk fk users.id
  login varchar(128) unique
  password varchar(128) nullable
  role varchar(10)=guest index | possible values: admin or guest
  created_at timestamp

roles
  slug varchar(10)

# define a standalone relation
fk credentials.role -> roles.slug""" 30 (List.nonEmpty errors)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
, div [] [ node "elm-textarea" [ value (Source.contentStr source), onInput (AUpdateSource source.id >> AmlSidebarMsg), onBlur (ASourceUpdated source.id |> AmlSidebarMsg) ] [] ]
, Editor.basic "source-editor" (Source.contentStr source) (AUpdateSource source.id >> AmlSidebarMsg) (ASourceUpdated source.id |> AmlSidebarMsg) """Write your schema using AML syntax
, div [] [ node "elm-textarea" [ value (Source.contentStr source), onInput (AUpdateSource source.id >> AmlSidebarMsg), onBlur (ASourceUpdated source.id |> AmlSidebarMsg) ] [] ]

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (5)
frontend/ts-src/components/elm-monaco.ts (2)

33-33: Avoid assigning the editor instance to the global window object

Assigning the editor instance to the global window object ((window as any).editor = this.editor) can lead to potential conflicts and is not recommended. Consider removing this assignment or providing access to the editor instance through a safer method if necessary.

Apply this diff to remove the global assignment:

- (window as any).editor = this.editor

19-20: Consider making the editor's height adjustable or responsive

The editor's height is set to a fixed value of '500px', which may not be suitable for all layouts. Consider making the height adjustable via an attribute or style, or use CSS to make it responsive to its container's size.

frontend/ts-src/components/elm-textarea.ts (3)

70-72: Enhance the counter display for better user clarity

Currently, the counter only shows a number, which might be unclear to users.

Consider adding descriptive text to the counter:

     private updateCounter() {
-        this.counter.textContent = `${this.textarea.value.length}`
+        this.counter.textContent = `Characters: ${this.textarea.value.length}`
     }

This provides users with a clearer understanding of what the counter represents.


15-30: Optimize style injection

Injecting styles as a string can be less maintainable for larger stylesheets.

Consider using a <link> to an external stylesheet or using CSS-in-JS libraries for better scalability.


7-73: Consider using ShadowRoot type assertion

When accessing this.shadowRoot, TypeScript may infer it as ShadowRoot | null. Since you attach a shadow root in the constructor, you can assert its existence.

Modify your code as follows:

-        this.shadowRoot?.appendChild(style)
-        this.shadowRoot?.appendChild(this.textarea)
-        this.shadowRoot?.appendChild(this.counter)
+        const shadow = this.shadowRoot as ShadowRoot
+        shadow.appendChild(style)
+        shadow.appendChild(this.textarea)
+        shadow.appendChild(this.counter)

This avoids unnecessary null checks after you've ensured the shadow root exists.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between ce2ba52 and 9c8ee9c.

📒 Files selected for processing (2)
  • frontend/ts-src/components/elm-monaco.ts (1 hunks)
  • frontend/ts-src/components/elm-textarea.ts (1 hunks)
🔇 Additional comments (1)
frontend/ts-src/components/elm-monaco.ts (1)

47-49: 🛠️ Refactor suggestion

Implement attribute change handling in attributeChangedCallback

Currently, the attributeChangedCallback method only logs attribute changes but does not update the editor state accordingly. To ensure that changes to attributes like value, readonly, and disabled are reflected in the editor, please implement the necessary handling within this method.

Comment on lines +28 to +32
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: `<div>Hello World</div>`,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Set the editor's initial value from the value attribute

The editor's initial content is currently hardcoded to <div>Hello World</div>. For better flexibility, consider setting the initial value from the value attribute of the custom element. This allows the content to be dynamically specified when the element is used.

Apply this diff to update the editor's initial value:

 this.editor = monaco.editor.create(this.container, {
     automaticLayout: true,
     language: 'html',
-    value: `<div>Hello World</div>`,
+    value: this.getAttribute('value') || '',
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: `<div>Hello World</div>`,
});
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: this.getAttribute('value') || '',
});

Comment on lines +1 to +51
/*import * as monaco from "monaco-editor";

// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md
// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.0#example-creating-the-editor-web-component

export function loadElmMonaco(): void {
customElements.define('elm-monaco', ElmMonaco)
}

class ElmMonaco extends HTMLElement {
private container: HTMLDivElement
private editor: monaco.editor.IStandaloneCodeEditor | undefined

constructor() {
super()
this.attachShadow({ mode: 'open' })

this.container = document.createElement('div')
this.container.style.width = '100%'
this.container.style.height = '500px'

this.shadowRoot?.appendChild(this.container)
}

connectedCallback() { // component added to the DOM
console.log('ElmMonaco.connectedCallback')
console.log('create monaco editor')
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: `<div>Hello World</div>`,
});
(window as any).editor = this.editor
console.log('monaco editor created')
}

disconnectedCallback() { // component removed from the DOM
console.log('ElmMonaco.disconnectedCallback')
this.editor?.dispose()
}

static get observedAttributes() {
// id, class
return ['value', 'readonly', 'disabled']
}

attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log('ElmMonaco.attributeChangedCallback', name, newValue)
}
}
*/
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove the comment block to enable code execution

The entire code is enclosed within a comment block (/* ... */) from lines 1 to 51. This prevents the code from executing. If this is unintentional, please remove the comment block to ensure the custom element ElmMonaco is properly defined and functional.

Apply this diff to remove the comment block:

-/*import * as monaco from "monaco-editor";
-
-// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md
-// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.0#example-creating-the-editor-web-component
-
-export function loadElmMonaco(): void {
-    customElements.define('elm-monaco', ElmMonaco)
-}
-
-class ElmMonaco extends HTMLElement {
-    private container: HTMLDivElement
-    private editor: monaco.editor.IStandaloneCodeEditor | undefined
-
-    constructor() {
-        super()
-        this.attachShadow({ mode: 'open' })
-
-        this.container = document.createElement('div')
-        this.container.style.width = '100%'
-        this.container.style.height = '500px'
-
-        this.shadowRoot?.appendChild(this.container)
-    }
-
-    connectedCallback() { // component added to the DOM
-        console.log('ElmMonaco.connectedCallback')
-        console.log('create monaco editor')
-        this.editor = monaco.editor.create(this.container, {
-            automaticLayout: true,
-            language: 'html',
-            value: `<div>Hello World</div>`,
-        });
-        (window as any).editor = this.editor
-        console.log('monaco editor created')
-    }
-
-    disconnectedCallback() { // component removed from the DOM
-        console.log('ElmMonaco.disconnectedCallback')
-        this.editor?.dispose()
-    }
-
-    static get observedAttributes() {
-        // id, class
-        return ['value', 'readonly', 'disabled']
-    }
-
-    attributeChangedCallback(name: string, oldValue: string, newValue: string) {
-        console.log('ElmMonaco.attributeChangedCallback', name, newValue)
-    }
-}
-*/
+import * as monaco from "monaco-editor";

+// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md
+// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.0#example-creating-the-editor-web-component

+export function loadElmMonaco(): void {
+    customElements.define('elm-monaco', ElmMonaco)
+}

+class ElmMonaco extends HTMLElement {
+    private container: HTMLDivElement
+    private editor: monaco.editor.IStandaloneCodeEditor | undefined

+    constructor() {
+        super()
+        this.attachShadow({ mode: 'open' })

+        this.container = document.createElement('div')
+        this.container.style.width = '100%'
+        this.container.style.height = '500px'

+        this.shadowRoot?.appendChild(this.container)
+    }

+    connectedCallback() { // component added to the DOM
+        console.log('ElmMonaco.connectedCallback')
+        console.log('create monaco editor')
+        this.editor = monaco.editor.create(this.container, {
+            automaticLayout: true,
+            language: 'html',
+            value: `<div>Hello World</div>`,
+        });
+        (window as any).editor = this.editor
+        console.log('monaco editor created')
+    }

+    disconnectedCallback() { // component removed from the DOM
+        console.log('ElmMonaco.disconnectedCallback')
+        this.editor?.dispose()
+    }

+    static get observedAttributes() {
+        // id, class
+        return ['value', 'readonly', 'disabled']
+    }

+    attributeChangedCallback(name: string, oldValue: string, newValue: string) {
+        console.log('ElmMonaco.attributeChangedCallback', name, newValue)
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/*import * as monaco from "monaco-editor";
// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md
// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.0#example-creating-the-editor-web-component
export function loadElmMonaco(): void {
customElements.define('elm-monaco', ElmMonaco)
}
class ElmMonaco extends HTMLElement {
private container: HTMLDivElement
private editor: monaco.editor.IStandaloneCodeEditor | undefined
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.container = document.createElement('div')
this.container.style.width = '100%'
this.container.style.height = '500px'
this.shadowRoot?.appendChild(this.container)
}
connectedCallback() { // component added to the DOM
console.log('ElmMonaco.connectedCallback')
console.log('create monaco editor')
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: `<div>Hello World</div>`,
});
(window as any).editor = this.editor
console.log('monaco editor created')
}
disconnectedCallback() { // component removed from the DOM
console.log('ElmMonaco.disconnectedCallback')
this.editor?.dispose()
}
static get observedAttributes() {
// id, class
return ['value', 'readonly', 'disabled']
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log('ElmMonaco.attributeChangedCallback', name, newValue)
}
}
*/
import * as monaco from "monaco-editor";
// https://github.com/elm-community/js-integration-examples/blob/master/more/webcomponents/README.md
// https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.0#example-creating-the-editor-web-component
export function loadElmMonaco(): void {
customElements.define('elm-monaco', ElmMonaco)
}
class ElmMonaco extends HTMLElement {
private container: HTMLDivElement
private editor: monaco.editor.IStandaloneCodeEditor | undefined
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.container = document.createElement('div')
this.container.style.width = '100%'
this.container.style.height = '500px'
this.shadowRoot?.appendChild(this.container)
}
connectedCallback() { // component added to the DOM
console.log('ElmMonaco.connectedCallback')
console.log('create monaco editor')
this.editor = monaco.editor.create(this.container, {
automaticLayout: true,
language: 'html',
value: `<div>Hello World</div>`,
});
(window as any).editor = this.editor
console.log('monaco editor created')
}
disconnectedCallback() { // component removed from the DOM
console.log('ElmMonaco.disconnectedCallback')
this.editor?.dispose()
}
static get observedAttributes() {
// id, class
return ['value', 'readonly', 'disabled']
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
console.log('ElmMonaco.attributeChangedCallback', name, newValue)
}
}

Comment on lines +41 to +48
connectedCallback() { // component added to the DOM
console.log('ElmTextarea.connectedCallback')
const value = this.getAttribute('value') || ''
console.log('connectedCallback.value', value)
this.textarea.value = value // value is empty here, not sure why as it should have a value from the start (cf frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm:308)
this.updateCounter()
this.textarea.addEventListener('input', this.updateCounter.bind(this))
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix event listener removal by storing the bound function reference

Currently, the bind method creates a new function every time it's called. This means that the function reference used in addEventListener is different from the one used in removeEventListener, causing the event listener to not be removed properly.

Apply this diff to store the bound function and ensure proper addition and removal of the event listener:

+    private boundUpdateCounter = this.updateCounter.bind(this)

     connectedCallback() { // component added to the DOM
         console.log('ElmTextarea.connectedCallback')
         const value = this.getAttribute('value') || ''
         console.log('connectedCallback.value', value)
         this.textarea.value = value // value might be empty here
         this.updateCounter()
-        this.textarea.addEventListener('input', this.updateCounter.bind(this))
+        this.textarea.addEventListener('input', this.boundUpdateCounter)
     }

     disconnectedCallback() { // component removed from the DOM
         console.log('ElmTextarea.disconnectedCallback')
-        this.textarea.removeEventListener('input', this.updateCounter.bind(this))
+        this.textarea.removeEventListener('input', this.boundUpdateCounter)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
connectedCallback() { // component added to the DOM
console.log('ElmTextarea.connectedCallback')
const value = this.getAttribute('value') || ''
console.log('connectedCallback.value', value)
this.textarea.value = value // value is empty here, not sure why as it should have a value from the start (cf frontend/src/PagesComponents/Organization_/Project_/Components/AmlSidebar.elm:308)
this.updateCounter()
this.textarea.addEventListener('input', this.updateCounter.bind(this))
}
private boundUpdateCounter = this.updateCounter.bind(this)
connectedCallback() { // component added to the DOM
console.log('ElmTextarea.connectedCallback')
const value = this.getAttribute('value') || ''
console.log('connectedCallback.value', value)
this.textarea.value = value // value might be empty here
this.updateCounter()
this.textarea.addEventListener('input', this.boundUpdateCounter)
}


disconnectedCallback() { // component removed from the DOM
console.log('ElmTextarea.disconnectedCallback')
this.textarea.removeEventListener('input', this.updateCounter.bind(this))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct event listener removal

Ensure that the event listener is removed correctly without re-binding the function.

Alternatively, you can define the bound function inline:

     disconnectedCallback() { // component removed from the DOM
         console.log('ElmTextarea.disconnectedCallback')
-        this.textarea.removeEventListener('input', this.updateCounter.bind(this))
+        this.textarea.removeEventListener('input', this.updateCounter)
     }

However, this only works if this.updateCounter doesn't require binding. Since it likely does, storing the bound function as shown earlier is the preferred solution.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +60 to +68
attributeChangedCallback(name: string, oldValue: string, newValue: string) { // can't see this function triggered :/
console.log('ElmTextarea.attributeChangedCallback', name, newValue, this.getAttribute('value') || '')
if (name === 'value') {
this.textarea.value = newValue
this.updateCounter()
} else {
this.textarea.setAttribute(name, newValue)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure attributeChangedCallback is triggered and handles initial attributes

You mentioned that attributeChangedCallback isn't being triggered. This can occur because attribute changes before the element is connected do not invoke the callback. Additionally, initial attributes aren't processed unless handled explicitly.

Refactor the code to handle initial attributes in connectedCallback and ensure attribute changes are captured:

     connectedCallback() { // component added to the DOM
         console.log('ElmTextarea.connectedCallback')
-        const value = this.getAttribute('value') || ''
-        console.log('connectedCallback.value', value)
-        this.textarea.value = value // value might be empty here
+        // Handle initial attributes
+        ElmTextarea.observedAttributes.forEach(attr => {
+            this.attributeChangedCallback(attr, null, this.getAttribute(attr))
+        })
         this.updateCounter()
         this.textarea.addEventListener('input', this.boundUpdateCounter)
     }

     static get observedAttributes() {
         return ['value', 'readonly', 'disabled']
     }

     attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
         console.log('ElmTextarea.attributeChangedCallback', name, newValue)
         if (name === 'value') {
             this.textarea.value = newValue || ''
             this.updateCounter()
         } else if (name === 'readonly' || name === 'disabled') {
             if (newValue !== null) {
                 this.textarea.setAttribute(name, '')
             } else {
                 this.textarea.removeAttribute(name)
             }
         }
     }

This approach ensures that both initial attributes and subsequent changes are handled appropriately.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
attributeChangedCallback(name: string, oldValue: string, newValue: string) { // can't see this function triggered :/
console.log('ElmTextarea.attributeChangedCallback', name, newValue, this.getAttribute('value') || '')
if (name === 'value') {
this.textarea.value = newValue
this.updateCounter()
} else {
this.textarea.setAttribute(name, newValue)
}
}
connectedCallback() { // component added to the DOM
console.log('ElmTextarea.connectedCallback')
// Handle initial attributes
ElmTextarea.observedAttributes.forEach(attr => {
this.attributeChangedCallback(attr, null, this.getAttribute(attr))
})
this.updateCounter()
this.textarea.addEventListener('input', this.boundUpdateCounter)
}
static get observedAttributes() {
return ['value', 'readonly', 'disabled']
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
console.log('ElmTextarea.attributeChangedCallback', name, newValue)
if (name === 'value') {
this.textarea.value = newValue || ''
this.updateCounter()
} else if (name === 'readonly' || name === 'disabled') {
if (newValue !== null) {
this.textarea.setAttribute(name, '')
} else {
this.textarea.removeAttribute(name)
}
}
}

@loicknuchel loicknuchel marked this pull request as draft November 29, 2024 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant