michalsn / codeigniter-htmx

HTMX helper library for CodeIgniter 4 framework
https://michalsn.github.io/codeigniter-htmx/
MIT License
75 stars 15 forks source link

Template `viewData` not inherited #75

Closed neznaika0 closed 2 weeks ago

neznaika0 commented 4 weeks ago

If I understood correctly, my example should have inherited data. But I only see the first ones. I think there is a problem when using extend() and include(). Since the normal behavior is to transfer data internally to the final template

Examples ```php class Home extends AbstractController { protected $data = []; public function index(): string { $this->data = ['foo' => 'FOO']; // 1. // return '' // . view('/bug/view1', $this->data + ['bar' => 'BAR']) // . view('/bug/view2', $this->data + ['baz' => 'BAZ']) // . view('/bug/view3', $this->data + ['too' => 'TOO']) // ; // 2. return '' . view_fragment('/bug/view1', ['fragment1'], $this->data + ['bar' => 'BAR']) . view_fragment('/bug/view2', ['fragment2'], $this->data + ['baz' => 'BAZ']) . view_fragment('/bug/view3', ['fragment3'], $this->data + ['too' => 'TOO']) . view_fragment('/bug/include', ['include'], $this->data + ['inc' => 'INC']) // . view('/bug/include', $this->data + ['inc' => 'INC']) ; } } ``` ```php // layout Document renderSection('content') ?> include('/bug/include') ?> ``` ```php // view1 extend('/bug/layout') ?> section('content') ?>
View 1
foo:
bar:
baz:
too:
inc:
endSection() ?> fragment('fragment1') ?>
Fragment 1
foo:
bar:
baz:
too:
inc:
endFragment() ?> ``` ```php // view2, view3 similar
View 2
foo:
bar:
baz:
too:
inc:
fragment('fragment2') ?>
Fragment 2
foo:
bar:
baz:
too:
inc:
endFragment() ?> ``` ```php // include fragment('include') ?>
Include
foo:
bar:
baz:
too:
inc:
endFragment() ?> ``` Variant 2, add `['inc' => 'INC']` data. No last `baz, too, inc` ``` View 1 foo: YES bar: YES baz: NO too: NO inc: NO Include Frag foo: YES bar: YES baz: NO too: NO inc: NO Fragment 1 foo: YES bar: YES baz: NO too: NO inc: NO Fragment 2 foo: YES bar: YES baz: YES too: NO inc: NO Fragment 3 foo: YES bar: YES baz: YES too: YES inc: NO Include Frag foo: YES bar: YES baz: NO too: NO inc: NO ``` If uncomment last `view()`. `baz, too, inc` appears ``` // the same as above, but last Include Frag foo: YES bar: YES baz: NO too: NO inc: NO Include Frag foo: YES bar: YES baz: YES too: YES inc: YES ```
neznaika0 commented 3 weeks ago

Added repo https://github.com/neznaika0/appstarter/tree/fragents-data Run ./spark serve and uncomment different examples

michalsn commented 3 weeks ago

Thank you - I'll look into it after the weekend.

neznaika0 commented 3 weeks ago

I'm close to a solution. The first error is that renderFragment() always returns the first fragment. There may be several of them, it is always an array.

[fragments:protected] => Array
    (
        [include] => Array
            (
                [0] => <div style="background-color: lightpink;width:500px;height: 20px;">
<b>Include Frag</b>
<b>foo: </b> YES    <b>bar: </b> YES    <b>baz: </b> NO    <b>too: </b> YES    <b>inc: </b> NO</div>

                [1] => <div style="background-color: lightpink;width:500px;height: 20px;">
<b>Include Frag</b>
<b>foo: </b> YES    <b>bar: </b> YES    <b>baz: </b> NO    <b>too: </b> YES    <b>inc: </b> YES</div>

            )

    )

[fragmentStack:protected] => Array
    (
    )
neznaika0 commented 3 weeks ago

Other questions.

  1. How should a fragment/include be displayed if a section is specified, but it is inside?
    
    <?= $this->extend('/htmx/layout') ?>

<?php $this->section('content') ?> <?php $this->fragment('fragment1') ?> ... <?php $this->endFragment() ?> <?= $this->include('/htmx/include') ?> <?php $this->endSection() ?>

2. How should the fragment/include be displayed if the section is specified, but it is outside?
```php
<?= $this->extend('/htmx/layout') ?>

<?php $this->section('content') ?>
...
<?php $this->endSection() ?>

<?php $this->fragment('fragment1') ?>
    ...
    <?= $this->include('/htmx/include') ?>
<?php $this->endFragment() ?>
<?= $this->include('/htmx/include') ?>
  1. How should view() and view_fragment() work in different cases?
  2. What should I do when there may be many fragments and they are in different visibility zones?

It is logical not to use fragments outside the section. And include() should be read 100%, but obey the rules of fragments and sections. There are no restrictions without specifying a section - we show everything

Draft PR #77

michalsn commented 3 weeks ago

Generally, view_fragment should work the same as view but only return the given fragment.

Thank you for starting work on this.

neznaika0 commented 3 weeks ago

I don't understand how templates work well. Their nesting should essentially be like in PHP. https://www.php.net/manual/en/language.variables.scope.php#language.variables.scope When specifying a section, the fragment should not be shown or supplemented with a stack. And include() should always be displayed if it follows the same rules for the section. I would discuss each case in detail, but this requires a behavior specification. How can this be done? With diagrams? Tests?

michalsn commented 3 weeks ago
  1. Only content for fragment1 should be displayed
  2. Only content for fragment1 should be displayed - in this case, it will contain also the include() inside the fragment1.
  3. They should work the same. The only difference is that view_fragment should return only fragments of content we asked for - nothing more. If the include() is outside the fragment, then it should be ignored.
  4. I don't understand the question.

From what I checked your PR is working fine. We just need some tests to cover the fixed bug.

neznaika0 commented 3 weeks ago

EDIT: The tests will fail if you add fragments to different places or split a fragment into several. The current templates are primitive

  1. One fragment can be splited into several parts in different zones. In a good way, I would ignore fragments if the first element is in the section and the others are out. The conclusion is to prohibit the separation of the fragments or correct the behavior with nesting.
<?= $this->extend('/htmx/layout') ?>

<?php $this->section('content') ?>
    ...
    <?php $this->fragment('fragment1') ?> // in section, good. has normal data
        ...
    <?php $this->endFragment() ?>
<?php $this->endSection() ?>

<?php $this->fragment('fragment1') ?> // out of section, bad. it may have other data that is not available without a section
    ...
<?php $this->endFragment() ?>
<?= $this->include('/htmx/include') ?> // the same "fragment1" may be inside or "content > fragment1"

It is very difficult to consider all cases. @kenjis or @lonnieezell can they tell me?

michalsn commented 3 weeks ago

Okay, I get it now. Yes, the fragments implementation is very simplified. Sections are not taken into account when generating results.

Although I can't imagine a scenario where I would use the same section name in several places - I just didn't foresee that.

neznaika0 commented 3 weeks ago

It is very convenient to add sections. For example, CSS, JS connection.

<?php $this->section('js') ?>
<script src="script1.js"></script>
<?php $this->endSection() ?>

// Other template with unique JS scripts
<?php $this->section('js') ?>
<script src="script2.js"></script>
<?php $this->endSection() ?>

// Output in layout
<script src="script1.js"></script>
<script src="script2.js"></script>

I keep thinking how exactly the fragment should work. Is it worth using only one last fragment? In my case, view()->view_fragment() does not have to output the first fragments, it should be overwritten next, not added.

It follows that endFragment() should not output echo $contents. Why? It is important for HTMX to replace elements. Case. We use <div id="alerts"> in the fragment. We just pass the conditions and display the fragment <div id="alerts" hx-swap-oob="true">. The previous fragment1 are not needed.

// $testString2 = 'Hello World';

Fragment #1 top
<?php $this->fragment('fragment_1'); ?>
<?= $testString2 ?? 'Hello' ?>, fragment1_1!
<?php $this->endFragment(); ?>
<?php $this->fragment('fragment_1'); ?>
<?= $testString2 ?? 'Hello' ?>, fragment1_2!
<?php $this->endFragment(); ?>
Fragment #1 bottom

// view('view')
Fragment #1 top
Hello World, fragment1_2!
Fragment #1 bottom

// view_fragment('fragment_1')
Hello World, fragment1_2!

// view('view')->view_fragment('view', 'fragment_1', ['testString2' => 'Goodbye World'])
Fragment #1 top
Goodbye World, fragment1_2!
Fragment #1 bottom

@datamweb Please join the discussion

michalsn commented 2 weeks ago

Although I can't imagine a scenario where I would use the same section name in several places - I just didn't foresee that.

I should have written “fragments” and not “section” - sorry for the confusion.

michalsn commented 2 weeks ago

Ok, I finally found some time to look at this in peace.

The way view fragments work is clear. If you use the same fragment name (example1) multiple times in a view file, then when you use the view_fragment('file', 'example1'), it should return content from all those fragment occurrences. Not from the first one, not the last one, but all of them (respecting the section).

The only function fragments are supposed to serve is to provide snippets of content that the developer has requested.

I will try to prepare the fixes this weekend.

neznaika0 commented 2 weeks ago

Just for fragments, I don't see any cases for additions. I think it will be necessary to make unique names for the fragments, then build the entire template and at the end, based on the fragments received, replace them. You only need a rule or setting for fragments - always use the first/last one, add all the found ones or something else.

<p>
<?= fragment('post') ?>
<?= endFragment() ?>
</p>
<?= fragment('post') ?> // What can be expanded if the fragment has already been output?
<?= endFragment() ?>

In my understanding, a fragment should not have extensibility, it is not a section. The fragment must be a unique part of the section/template.

I'm very interested in the fix - I'm stuck in my own project

michalsn commented 2 weeks ago

Fragments were never meant to be extendable.

They are simply meant to return a batch of content in between - that's all.

Unique names for fragments are something obvious - IDK if there is a scenario where I would use multiple fragments with the same name... but I will add support for them.

neznaika0 commented 2 weeks ago

add in tests extra templates:

// huge_layout
Layout top

    <?php echo $this->renderSection('content') ?>

    <?php echo $this->include('huge_include') ?>

    <?php echo $this->fragment('fragment_one'); ?>
        Fragment one (from "huge_layout")
    <?php echo $this->endFragment(); ?>

Layout bottom
// huge_view
<?php echo $this->extend('huge_layout') ?>

<?php echo $this->section('content'); ?>
    Section (from "huge_view")
<?php echo $this->endSection(); ?>

View top

    <?php $this->fragment('fragment_one'); ?>
        Fragment one (from "huge_view")
    <?php $this->endFragment(); ?>

    # include start in "huge_view"
        <?php echo $this->include('huge_include') ?>
    # include end in "huge_view"

View bottom
// huge_include
Include top

    <?php $this->fragment('fragment_one'); ?>
        Fragment one (from "huge_include")
    <?php $this->endFragment(); ?>

Include bottom

EDIT: An approximate job for many fragments. We save the places of use until output. Then, depending on the inheritance settings, we replace the unnecessary placeholders

// Before
<body>
    <?= $this->fragment('post') ?>
    <post />
    <?= $this->endFragment() ?>
    <?= $this->fragment('post') ?>
    <script></script>
    <?= $this->endFragment() ?>
</body>

// in progress, delete unnecessary
<body>
    {{ post_fragment_KAOF34AWDGK }}
    {{ post_fragment_8UJHAW657FG }}
</body>