Load a different page (including a form) into a modal


#1

Hi there,

as I am not into ATK JS as much as I would like to yet, I will ask the experts :slight_smile:
2 Screenshots will help to discribe what I want to do:

In my Project, I use Exclamation Marks (!) in different colors to show there may be data missing/wrong etc:

As you can see, quite some. This is a calendar like overview page, but nearly every other page displays these ! as well.
Now I want a Modal with a form to show up when clicking these !. In the form, the notifications can be disabled. I partly implemented this functionality on pages where only a single Item is shown, looks like:

Now, I want to offer this modal everywhere a ! is shown by clicking on it. To make this fast, I want the modal to load the content of a different PHP script which is just there to load the notifications and display.
In pure Pseudo-JS this would be

!.on(click) {
    var form_html = ajax('load_notifications.php?class=classname&id=some_id');
    modal.html(form_html);
    modal.show();
}

And of course the form ajax-submits to ‘load_notifications.php’…

Now, can this be easily done with built-in ATK functionality, like setting a different URL to a callback etc?

Thanks again for all your help!
Philipp


#2

there are two modal implementations in atk

  1. View - Modal. This works with a pre-existing Div , simply shows it and can then populate with contents.

  2. jsModal. This creates entirely new modal Div then populates it.

I think you are talking about the second option here.

If you need to invoke it from JS: see this file: https://github.com/atk4/ui/blob/develop/js/src/plugins/createModal.js

the invocation is wrapped into PHP here: https://github.com/atk4/ui/blob/develop/src/jsModal.php#L16

Hopefully this can give you some ideas, or at least you can copy some code out.


#3

JsModal looks great as it has the URL param :slight_smile: will play around with this, thx


#4

Something like this should work too.

//modal will be render into page but hide until $modal->show() is activate.
$modal = $app->add(['Modal', 'title' => 'My Form']);
$b = $app->add['button'];

//When $modal->show() is activate, it will dynamically add this content to it.
$modal->set(function ($modal) {
    $form = $modal->add('Form');
   //add field or set model
    $form->onSubmit(function ($form) {
        return success,
    });
});

$b->on('click', $modal->show());

see more sample in http://ui.agiletoolkit.org/demos/modal2.php


#5

I love when things go exceptionally easy. Whenever it comes to JS loading content, this is the case with Agile UI. I got this working within an hour. Hope this can help as small tutorial for others, so I will write a bit more detailed.

Ok, so the first part is to display the exclamation marks and the JS needed for it to open the modal and open the target script:
I created a new View which takes care of it. Wherever I need a ! which opens the modal, I add this to another view: $some_view->add(new \Pmg\View\NotificationMark($level, 'Tour', $tour->get('id')));

And the NotificationMark class:

<?php
namespace Pmg\View;

class NotificationMark extends \atk4\ui\View {

    //1-3, level of the notification mark, changes ! color
    public $level;
    //classname, e.g. "Tour", used to load Notifications for record in Modal
    public $classname;
    //id of the record to load notifications of
    public $record_id;
    
    public function __construct($level, $classname, $record_id, $label = null, $class = null) {
        $this->level = $level;
        $this->classname = $classname;
        $this->record_id = $record_id;
  
        parent::__construct($label, $class);
    }
    
    public function init() {
        parent::init();
        $this->content = '!';
        $this->addClass('eoo-level'.$this->level.'-mark');
        $this->on('click', new \atk4\ui\jsModal('Meldungen', URL_BASE_PATH.'notification_modal.php?class='.urlencode($this->classname).'&id='.urlencode($this->record_id)));
    }
}

So this view is just a div with only a ! in it, having the “click” event listener. On clicking, a modal is opened which then AJAX loads the contents of notification_modal.php

As these AJAX calls expect JSON formatted content, we need to JSON encode the output of notification_modal.php in a way Agile UI likes. Of course, there is a function for it: View->renderJSON().

As notification_modal.php is never opened directly but only called via AJAX, the App is just terminated, outputting the result of renderJSON:

<?php
//Load Main App
require 'config.php';
$app = new \Pmg\TourManagement();

$content_wrapper = $app->add(['View']);
... add more views to $content_wrapper ...

$app->terminate($content_wrapper->renderJSON());

This causes the output to be formatted the way the Agile UI Javascript calling it wants it.
The Modal now has the content of$content_wrapper.

In my case, it is a form. Here we need to use a nice feature of Agile UI: stickyGET. The parameters needed to load the record (class and id) are needed again when to form is submitted. Setting these as stickyGET, Agile UI adds these parameters to every URL created.

<?php
//Load Main App
require 'config.php';
$app = new \Pmg\TourManagement();


/*
 * For Form submission the GET parameters need to be set,
 * atk takes care of this with stickyGET
 */
$app->stickyGET('class');
$app->stickyGET('id');


/*
 * Load class and notifications if possible
 */
try {
    $classname = '\\Pmg\\'.$_GET['class'];
    if(class_exists($classname)) {
        $model = new $classname($app->db);
        $model->load($_GET['id']);
        $model->loadNotifications();
    }
    else {
        throw new \Exception('class '.$classname.' not found');
    }
}

//error message if any code in try block throws exception, e.g. if record
//with passed ID is not found
catch(Exception $e) {
    $error_message = $app->add(['View', 'Fehler: Die Daten konnten nicht geladen werden']);
    $app->terminate($error_message->renderJSON());
}


/*
 * Add Form
 */
$content_wrapper = $app->add(['View']);

$explanation = $content_wrapper->add(['View', 'Meldungen können mit dem "Häkchen" als gelesen markiert werden.
    Wenn eine Meldung als gelesen markiert wird, wird sie nicht mehr angezeigt.
    Gelesene Meldungen können wieder als ungelesen markiert werden.']);
$explanation->addClass('small-margin-bottom');

$f = $content_wrapper->add('Form');
$f->layout->inline = true;
$f->buttonSave->set('Speichern')->addClass('eoo-createbutton-disabled');

foreach($model->notifications as $notification) {
    $chbox = new \atk4\ui\FormField\CheckBox();
    $chbox->label = $notification->translateType();
    $f->addField('notification'.$notification->get('id'), $chbox)->addClass('eoo-level'.$notification->get('level').'-text');

    //"disable" checkbox text if needed
    if($notification->get('deactivated')) {
        $chbox->addClass('eoo-disabled');
        $chbox->set(true);
    }
    
    //JS: Grey texts if selected, Green Save button as soon as change happened
    $chbox->on('change', (new \atk4\ui\jQuery('.atk-modal .eoo-createbutton-disabled'))->addClass('eoo-createbutton-highlight')->removeClass('eoo-createbutton-disabled')); 
    $chbox->on('change', (new \atk4\ui\jQuery('#'.$chbox->name))->toggleClass('eoo-disabled'));
}


/*
 * Form submitting: Save each notification where "deactivated" was changed
 */
$f->onSubmit(function () use ($f, $model) {
    foreach($f->fields as $name => $field) {
        //if its a notification field
        if(substr($name, 0, 12) == 'notification') {
            $n_id = substr($name, 12);
            foreach($model->notifications as $n) {
                if($n->get('id') === $n_id && $n->get('deactivated') !== $field->getValue()) {
                    $n->set('deactivated', $field->getValue());
                    $n->save();
                }
            }
        }
    }
    
    //close modal after saving
    $modal_chain = new \atk4\ui\jQuery('.atk-modal');
    $modal_chain->modal('hide');
    return $modal_chain;
});

/*
 * JSON Formatted output
 */
$app->terminate($content_wrapper->renderJSON());
?>

A small screencast will follow when I completely implemented this in a page.


#6

Ok,

here is the Screencast:

As you can see, clicking each ! displays different content, generated in notification_modal.php. The real good thing about this is that its superfast as notification_modal.php is a small script with just 2 DB queries.
The page the ! are on is big and might grow huge depending on how the users want it. If the Modal content was created inside the script showing that big page, opening each modal would take more time as the whole script is always executed (is that right romans? :slight_smile: )

Ok, so this solution works perfectly for loading content from a different script into a Modal.

For my case, I need to alter the code a bit, but thats due to other reasons:
First: As I said, at least this page is huge (like 500+ records displayed), and for each one with a ! 4 Lines of JS are created:

 $("#_a4475521__icationmark").on("click",function(event) {
    event.preventDefault();
    event.stopPropagation();
    $(this).atkCreateModal({"uri":"http://localhost/TourManagement
    /notification_modal.php?class=Tour\x26id=409","title":" Meldungen","mode":"json","uri_options":[]});
  });

So some 100 Javascripts all doing the same.

Second: more important, as I display a form in which data can be changed, these changes need to be displayed in the original page - as the original page is big I do not want to reload it completely.
The tricky part is: a data record can be displayed several times on this page, see the “Guides” column on the right.
As JS currently has no idea which record these <div>'s looking like boxes displays, there is not a sensible way I can think of to change all according !.

So, time for some improvement:
The plan is to have one single JS which opens the modal and loads the data. For this, this JS needs to know which record is represented. Here the HTML data- tags come in handy: the div displaying the record will get data-class and data-id. This is not the wheel reinvented, its just copied from the Agile UI lists. Have a look at Table->jsRow(), this does sometihng quite similar.
This should work for part 1, opening the modal with a single Javascript.

Part two is to alter the main page when the modal is closed after form submission. load_notifications.php will check if anything needs to be altered (either the color of the ! changed or the ! removed), create a JQuery doing this which is passed back to the main page.

Thats the plan for now, lets see how it works :slight_smile:


#7

It works :slight_smile:

Ok, so the prior solution works perfect in most cases when you just want to load the output of a different php script into a modal, but as mentioned before, my needs a bit special.

So, the main work was to create some HTML for everything that represents a record that can have these exclamation marks.
The HTML for each record looks like this now:

<a class="eoo-list-item" data-class="Group" data-id="31" href="groups.php?id=31">
    <div class="upcoming-group-name">Grace Kelly (4)</div>
    <div class="notification-mark-container"><span class="eoo-level2-mark">!</span></div>
</a>

So the data-class and data-id tags can be used to identify the record this html displays. They all have the class eoo-list-item.

Ok, lets use this in JQuery: I wrote a small script outside Agile UI which creates an onClick event listener to all .eoo-level1-mark, eoo-level2-mark and eoo-level3-mark classes (they represent the ! in the 3 different colors).
When its clicked, it loads the data-class and data-id values found in some parent and uses these values to create the URL for the modal to open:

/*!
 * Opens the modal which displays the modal with the form to
 * acticate/deactivate notifications
 */
$(function() {
    //the 3 matching classes (representing different colors)
    $(".eoo-level1-mark, .eoo-level2-mark, .eoo-level3-mark").on("click",function(event) {
        event.preventDefault();
        event.stopPropagation();
        //data-class and data-id is 2 elements up the DOM tree,
        //so lets select the first matching parent. Any representation of a
        //data object has the class eoo-list-item
        var className = $(this).parents(".eoo-list-item").eq(0).data("class");
        var recordId = $(this).parents(".eoo-list-item").eq(0).data("id");
        //no need to urlencode the parameters here as we control exactly what they are like
        $(this).atkCreateModal({"uri":"http://localhost/TourManagement/notification_modal.php?class="+className+"\x26id="+recordId,"title":" Meldungen","mode":"json","uri_options":[]});
    });
});

So far for the first task, this works. Instead of a lot of Javascripts as before there is now only one that takes care of all the ! and opening their modals.

Second task: As I want to alter the base page after some action took place in the modal, I needed to create some JS in there that does this. Luckily again, Agile UI integrates this into forms already: The Form->onSubmit() function can return Javascripts that are executed when the form is submitted.

So lets extend the onSubmit function in load_notifications.php:

/*
 * Form submitting: Save each notification where "deactivated" was changed
 */
$f->onSubmit(function () use ($f, $model) {
    //used to determine if max level has changed after saving
    $level_before = $model->getMaxNotificationLevel();

    //iterate all form fields
    foreach($f->fields as $name => $field) {
        //if its a notification field
        if(substr($name, 0, 12) == 'notification') {
            $n_id = substr($name, 12);
            if(isset($model->notifications[$n_id]) && $model->notifications[$n_id]->get('deactivated') !== $field->getValue()) {
                $model->notifications[$n_id]->set('deactivated', $field->getValue());
                $model->notifications[$n_id]->save();
            }
        }
    }
    //find new max level
    $new_level = $model->getMaxNotificationLevel();

    /*
     * Returning one or more JSs, collect them in this array
     */
    $js_returns = [];

    //create jQuery to update "parent" page exclamation marks
    if($new_level != $level_before) {
        //all notifications deactivated, hide !
        if(!$new_level) {
            $js_returns[] = (new \atk4\ui\jQuery('.eoo-list-item[data-class=\''.$_GET['class'].'\'][data-id=\''.$_GET['id'].'\']'))
                            ->children('.notification-mark-container')
                            ->children('.eoo-level1-mark, .eoo-level2-mark, .eoo-level3-mark')
                            ->remove();
        }
        //set ! to new color
        else {
            $js_returns[] = (new \atk4\ui\jQuery('.eoo-list-item[data-class=\''.$_GET['class'].'\'][data-id=\''.$_GET['id'].'\']'))
                            ->children('.notification-mark-container')
                            ->children('.eoo-level1-mark, .eoo-level2-mark, .eoo-level3-mark')
                            ->removeClass('eoo-level1-mark eoo-level2-mark eoo-level3-mark')
                            ->addClass('eoo-level'.$new_level.'-mark');
        }
    }
    
    //close modal after saving
    $js_returns[] = (new \atk4\ui\jQuery('.atk-modal'))->modal('hide');
    
    return $js_returns;
});

This time the JQuery code is created using Agile UI’s JQuery class. Works great :slight_smile: This JQuery first finds the <div> that displays the record, then travels down the DOM to the ! and changes it accordingly. All I had to do to make this work is to return these JQuerys in Form->onSubmit, Agile UI takes care of executing them.

Here is a screencast of the new solution, notice that “Neil Diamond” exits 3 times on the screen, and the ! of all 3 are changed (because they all fit the JQuery selector .eoo-list-item[data-class='Group'][data-id='9']

notification_modal_screencast_2