Skip to content

Conversation

@hhaensel
Copy link
Member

@hhaensel hhaensel commented May 8, 2025

With this PR all methods, watchers, computed properties and lifecyclehooks are transferred from mixins to the incorporating app. Prefixes and postfixes are applied to variables when they are prefixed by this in the functions. Method names are also prefixed when they are defined as Symbols or Strings; they are left untouched when they are defined as JSONText.

Example: HistoryTab - A tab somponent with history navigation

using Stipple, Stipple.ReactiveTools, StippleUI

Stipple.enable_model_storage(false)

@app_mixin HistoryTab begin
    @in var"" = "home"
    @in _navigate = ""

    @onchange isready begin
        isready || return
        notify(var"")
    end
    
    @onchange _navigate begin
        if _navigate != ""
            var""[!] = _navigate
            @push :var""
        end
    end

    @onchange var"" begin
        @info "$(:var"") changed to '$(var"")'"
        # empty _navigate to make sure, it is executed the next time a non-empty value is set
        _navigate = ""
        @run """
        this['_index_'] = this['_index_'] + 1 || 0;
        state = {field: '$(:var"")', value: '$(var"")', index: this['_index_']}
        console.log('push state: ', state)
        history.pushState(state, '', '#' + this['_index_']);
        """
    end
end

@mounted HistoryTab js"""
    window.addEventListener('popstate',
        (event) => {
            if (event.state && event.state.field) {
                console.log('new state: ', event.state)
                if (this[event.state.field] == event.state.value) {
                    old_index = this['_index_'];
                    this['_index_'] = event.state.index;
                    if ((old_index >= event.state.index) && (event.state.index > 0)) {
                        history.back();
                    } else {
                        history.forward();
                    }
                } else {
                    this[event.state.field + '_navigate'] = event.state.value
                    this['_index_'] = event.state.index
                }
            }
        }
    );
    """

@methods HistoryTab [
    :_hello => js"""
    function () {
        console.log('The current tab is: \'' + this. + '\'')
        console.log('The tab\'s variable name is: \'' + 'this. '.slice(5, -1) + '\'')
        console.log('Last navigation was to: \'' + this._navigate + '\'')
    }
    """,
    js"hello_everyone" => js"""
    function () {
        console.log('Tab-independent greeting!')
    }
    """
]

@app_mixin MyApp begin
    @in i = 1
    @mixin tab1::HistoryTab
    @mixin tab2::HistoryTab

    @onchange i begin
       println("i: ", i)
    end
end

@mounted MyApp [
    js"""
    console.log('isready: ', this.isready)
    """
]

function ui()
    row(class = "st-module", cell(class = "q-pa-md q-ma-md", [
        tabgroup(:tab1, [
            tab(label = "Home", name = "home"),
            tab(label = "About", name = "about"),
            tab(label = "Contact", name = "contact")
        ])

        tabgroup(:tab2, [
            tab(label = "Home", name = "home"),
            tab(label = "About", name = "about"),
            tab(label = "Contact", name = "contact")
        ])
    ]))
end

@page("/", ui, model = MyApp)

up(open_browser = true)

@hhaensel
Copy link
Member Author

hhaensel commented May 9, 2025

StippleUI needs a small correction in Tables.jl
but that's only a fix for js_watch and js_created for backward compatibility with Quasar 1.
Only event support is still missing.
I guess the way to add them is analogous to the definition of handlers.

@hhaensel hhaensel requested a review from Copilot September 15, 2025 05:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces automatic inclusion of lifecycle hooks, methods, computed properties, and watchers from mixins into the incorporating app, with support for prefixes and postfixes on variables and method names. The implementation refactors the rendering system to support mixin composition and provides variable name transformation when prefixes/postfixes are applied.

Key changes:

  • Added Mixin struct and mixin processing logic to automatically include all Vue.js options from mixins
  • Refactored join_js function to support flattening, uniqueness, and key replacement for mixin variable transformations
  • Updated all JS method signatures to use type-based dispatch (::Type{<:T}) instead of instance-based dispatch for better mixin support

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/stipple/rendering.jl Core mixin rendering logic, Mixin struct definition, and refactored join_js function
src/stipple/reactivity.jl Updated mixin handling in @var_storage macro and type generation
src/stipple/jsmethods.jl Changed all JS method signatures from instance-based to type-based dispatch
src/ReactiveTools.jl Updated macro definitions to use type-based dispatch for Vue options
src/Elements.jl Added jse_str macro export

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@hhaensel hhaensel requested a review from Copilot September 16, 2025 21:35
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +186 to +193
function js_mixin(m::Mixin, js_f, delim)
M, prefix, postfix = m.M, m.prefix, m.postfix
vars = get_known_js_vars(M)
empty_var = Symbol("") vars
empty_var && setdiff!(vars, [Symbol("")])

replace_rule1 = Regex("\\b(this|GENIEMODEL)\\.($(join(vars, '|')))\\b") => SubstitutionString("\\1.$prefix\\2$postfix")
replace_rule2 = Regex("\\b(this|GENIEMODEL)\\. ") => SubstitutionString("\\1.$prefix$postfix ")
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The regex patterns contain magic strings 'this' and 'GENIEMODEL' that are duplicated. Consider extracting these into named constants to improve maintainability and reduce the risk of typos.

Suggested change
function js_mixin(m::Mixin, js_f, delim)
M, prefix, postfix = m.M, m.prefix, m.postfix
vars = get_known_js_vars(M)
empty_var = Symbol("") vars
empty_var && setdiff!(vars, [Symbol("")])
replace_rule1 = Regex("\\b(this|GENIEMODEL)\\.($(join(vars, '|')))\\b") => SubstitutionString("\\1.$prefix\\2$postfix")
replace_rule2 = Regex("\\b(this|GENIEMODEL)\\. ") => SubstitutionString("\\1.$prefix$postfix ")
const JS_THIS = "this"
const JS_GENIEMODEL = "GENIEMODEL"
function js_mixin(m::Mixin, js_f, delim)
M, prefix, postfix = m.M, m.prefix, m.postfix
vars = get_known_js_vars(M)
empty_var = Symbol("") vars
empty_var && setdiff!(vars, [Symbol("")])
js_var_pattern = "\\b($(JS_THIS)|$(JS_GENIEMODEL))\\.($(join(vars, '|')))\\b"
js_var_space_pattern = "\\b($(JS_THIS)|$(JS_GENIEMODEL))\\. "
replace_rule1 = Regex(js_var_pattern) => SubstitutionString("\\1.$prefix\\2$postfix")
replace_rule2 = Regex(js_var_space_pattern) => SubstitutionString("\\1.$prefix$postfix ")

Copilot uses AI. Check for mistakes.
Comment on lines +246 to +249
mounted_auto = """setTimeout(() => {
this.WebChannel.unsubscriptionHandlers.push(() => this.handle_event({}, 'finalize'))
console.log('Unsubscription handler installed')
}, 100)
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded timeout value of 100ms is a magic number. Consider making this configurable or extracting it as a named constant with documentation explaining why this specific delay is needed.

Suggested change
mounted_auto = """setTimeout(() => {
this.WebChannel.unsubscriptionHandlers.push(() => this.handle_event({}, 'finalize'))
console.log('Unsubscription handler installed')
}, 100)
# Timeout for mounting auto handler (ms). 100ms is chosen to ensure Vue's mounted lifecycle is complete before installing the unsubscription handler.
const MOUNTED_AUTO_TIMEOUT_MS = 100
mounted_auto = """setTimeout(() => {
this.WebChannel.unsubscriptionHandlers.push(() => this.handle_event({}, 'finalize'))
console.log('Unsubscription handler installed')
}, $(MOUNTED_AUTO_TIMEOUT_MS))

Copilot uses AI. Check for mistakes.
Comment on lines +658 to +660
if isdefined(M, modelname)
insert!(ex.args, 3, :(Base.delete_method.(methods(Stipple.mixins, (Type{<:$modelname},), $M))))
end
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

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

[nitpick] The magic number 3 for the insertion position is unclear. Consider using a named constant or adding a comment explaining why position 3 is significant in the expression structure.

Copilot uses AI. Check for mistakes.
@hhaensel hhaensel marked this pull request as ready for review September 17, 2025 04:09
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.

2 participants