FriendsOfTYPO3 / frontend_editing

TYPO3 CMS Frontend Editing
https://friendsoftypo3.github.io/frontend_editing/
102 stars 38 forks source link

Decouple design from controller #488

Open Messj1 opened 3 years ago

Messj1 commented 3 years ago

Hi

As part of improve code quality #432 and to improve extend ability there should be more separation of design and frontend-editing functionality.

I see following wrong dependencies flow:

In my opinion we should not attache events on server generated templates. The frontend editing GUI appearance should be part of the design or else you can only change color :unamused:

So what should GUI be?

It is used as

So it has to contain only bootstrap code and base composition logic code

What should be part of design

Everything that is generated on the server. So if it has to communicate we have to use id attribute because it should be unique or else it is to unclear what to do in GUI.

So as a good example lets have look at a simple slide and open problem.

checkbox hack

So we could use a good old classic hack to save and transfer the state (usable in GUI with t3-frontend-editing__right-bar as id)

#t3-frontend-editing__right-bar + .t3-frontend-editing__right-bar {
    transition: right 0.2s linear;
}

#t3-frontend-editing__right-bar:checked + .t3-frontend-editing__right-bar {
    right: 0;
}

#t3-frontend-editing__right-bar:checked + .t3-frontend-editing__right-bar .top-right-title .right-bar-button {
    left: 0;
}
<input type="checkbox" id="t3-frontend-editing__right-bar--open"/>
<div class="t3-frontend-editing__right-bar">
  <div class="top-right-bar-wrapper ">
    <div class="top-right-relative">
      <div class="top-right-title">
        <label for="t3-frontend-editing__right-bar--open" class="right-bar-button icons icon-icons-arrow-double"></label>
        ...
      </div>
      ...
    </div>
    ...
  </div>
</div>

Also possible to use radio instead of checkbox to get an real accordion :wink:

details and summary elements

Good old details elements have the advanced, that the have an open attribute which indicate open or close.

details[open] .icon-icons-arrow-down:before {
    content: "\e90f";
}
<details open>
  <summary class="accordion">
    ...
    <div class="element-action">
      ...
      <span class="icons icon-icons-arrow-down trigger"></span>
    </div>
  </summary>
  <div class="accordion-content">
  ...
  </div>
</details>

separate javascript based widgets

There are plenty technologies for js based widgets. From Web Components to attached function calls. From scripts to suits.

Conclusion

GUI should have different strategies to fetch data from the attached bindings like:

At least there should be the opportunity to write an own editor and use only the base logic from frontend editing to transfer and handle data. I would love to see an different frontend editing GUI implementation.

Any opinions?

Messj1 commented 3 years ago

I created some examples an put them on gist: https://gist.github.com/Messj1/de7a069c2199851fe3711a3729d96271

Once downloaded you can put them in <project-folder>/storybook/stories/design/RightBar

@MattiasNilsson can you please check this out and give me our opinions?

MattiasNilsson commented 3 years ago

@Messj1 It looks great! Go ahead with it, what is the next step you plan to do with this?

Messj1 commented 3 years ago

@MattiasNilsson Good question ... I think the goal should be to simplify the whole HTML handling in a structural way. => in general separate: Data, Logic, Control and Design

In fact i don't know what would be better, to have a listening or a callable system. Maybe both :zany_face:

Any comment is welcome!

Next steps

There are generated HTML files from the server and some CSS which handle the look and feel.

Data exchanging

So next step would be to declare the listening data flow respectively data exchange. For example:

simple state binding with local storage save and restore:

<input type="checkbox" data-gui-datatype="input" data-gui-persist="local" name="wizzard.plugins" />

simple class toggle:

<div data-gui-datatype="bind" data-gui-name="screenloading" data-gui-class="toogleClassname">
</div>

simple attribute (open, disabled, checked, selected) toggle:

<details data-gui-datatype="input" data-gui-attribute="open">
</details>

Or something more complex example with component involved

<!-- listening component state -->
<input type="checkbox" data-gui-datatype="bind" data-gui-component="D3IndentedTree" name="isSearchRunning" />
<!-- listening event and call component -->
<input type="text" data-gui-datatype="input" data-gui-component="D3IndentedTree" name="treeFilter" />

Maybe some structure like this

data-gui: {
    //indicates an data exchange element
    "datatype": {
        "default": null
        "value": ["bind", "input"]
    },
    //component which is used to call and fetch data
    "component": {
        "default": "GUI",
        "value": ["GUI", "Editor", "D3IndentedTree"]
    },
    //Name of action, variable or persist key
    "name": {
        "default": element is input? input.name
        "value": *
    },
    //saves the "return" value in the selected storage. if no return save the getter
    "persist": {
        "default": null
        "value": ["local", "session", "server"]
    },
    //toggle the CSS class
    "class": {
        "default": null
        "value": "*",
        "condition": [
            "return" value is boolean,
        ]
    },
}

DataBinder

Afterward DataBinder and DataHandlers module is used to fetch the data:

  1. initialize

    1. register data change listener --> trigger next steps
  2. fetch data

    1. look up predefined HTML data attributes
    2. or check if it is an input element
  3. trigger data event

So maybe there is also an Factory involved to get the right DataHandler according to data-gui-datatype.

GUI

The binding could be done in 3 ways as followed:

  1. initialized find and bind in GUI after DOM is ready

    // @module Typo3/CMS/FrontendEditing/GUI
    init: function() {
    ...
    this.findAndBindData();
    }
  2. register it later somewhere - we don't care :wink:

    // inline
    GUI.appendData(dataHandler);
  3. or like runtime just in time --> for datatype input only.

<input onclick="GUI.handleData" />

At least there would be much less code in GUI. In my opinion it should only bootstraps the whole application.

MattiasNilsson commented 3 years ago

@Messj1 Great idea, right now all is interconnected.

Messj1 commented 3 years ago

@MattiasNilsson Yes, that is the reason why it is currently not testable. It is also (k)im possible to extend. I made some overview what the new GUI architecture could look like. It is currently in draft:

I split into several categories to show how they should work independently:

FrontendEditing - bootstrap process

Hint: the DataHandler get coupled with GUI due the DataBinder, so as they are truly independent! :muscle: :godmode:

What do we win with all this heavy load bootstrap: :eyes:

I see no problem to make this changes without breaking anything because we are able to implement 2 ComponentFactory and 2 DataBinder, one as legacy mode and one with the new data-* approach.

By the way we should also replace the javascript GlobalEventHandler (onclick, ...) with help of DataBinder cause of CSP.

Maybe something like

<div data-gui-datatype="trigger" data-gui-component="CE" data-gui-name="dragAndDrop">
   ...
</div>

Any feedback is welcome :wink:

Messj1 commented 3 years ago

I have now extended the diagram with the missing modules to get a real overview of the complete idea.

the compatibility check (red dashed line) is only the consequence of the idea to create factories outside the loader in server generated code. Would also be possible to handle in loader.

As we can see, the main work part of GUI is handled in components (see relation count). DataHandler coupling with Component is handled by GUI controller. HTML coupling with component is handled by DataHandler.

The Component to EditorRegistry relation exists only for EditorComponent.

Missing part: How does Component get partials (related component like an editor toolbar)

And yes, I know that the Business Layer and especially Data Layer part is not finished. But the Idea is to get rid of undocumented calls since this is planed highly extendable. So everything in Business Layer get started by Action. Action only! :exploding_head:

Updated on 2020-03-02: simplified flow; abstracted data access in business layer with Interpreter 20200302 - frontend_editing - architecture

MattiasNilsson commented 3 years ago

@Messj1 The only thing I can add is awesome! Just go ahead with it :)

Messj1 commented 3 years ago

I finished the big picture as an communication and interface overview. There are some open issues like versioning. Eg. It is possible that Executers of different versions or implementation get used parallel if components uses different Action (Interface) version.

Note: Engine is a extendable State Machine and Executer package contains Interpreter (API) and Action (ActionData Interface) 20200316-T3 Frontend Editing - Big Picture

I also made a rough component overview. 20200316-T3 Frontend Editing - Component Overview

More details make no sense without prototype.

MattiasNilsson commented 3 years ago

@Messj1 This is just something I have had in my head before. So cool! Which tool are you using for this? Just curious :)

Messj1 commented 3 years ago

@MattiasNilsson Yeah, cause I used to base structure of already existing components. So names are already known. :wink: The big change is the Engine and Excecuter part which will gone be the game changer. BTW is every package planed to be replaceable. So everything can be connect as it prefer.

I use diagrams.net. Previously known as draw.io. You can find the latest version in my branch: https://github.com/Messj1/frontend_editing/tree/488-Decouple-design-from-controller/Documentation/Assets

I use only the simplest functions. There are plenty of nice functions like the native mermaid support but this becomes handy in a later phase. There is also a layering system and you are able to preview data online with a data link. For example with the layered LegacyDataBinder This becomes the next steps to close this issue an start another one with goal to rewrite the monolithic system into a modular (extendable) one.

MattiasNilsson commented 3 years ago

@Messj1 Thanks for all the nice work! 👍

Messj1 commented 3 years ago

@MattiasNilsson found some small time slice to implement a simple prototype. Can you please have a look on it?

https://github.com/Messj1/frontend_editing/tree/488-Decouple-design-from-controller/Resources/Public/JavaScript/DataBinder

There is following structure:

I also added xstate.fsm as a nice Finit State Machine (FSM) :nerd_face: As a test component i implemented ToggleState and glued it in storybook with the DataBinderWrapper. The main idea (currently) is to have an FSM as frontend (GUI) models and a extendable FSM as business model.

Storybook Test I have adapted the following Story /story/design-bar-right--default

I'm insecure what would be better:

  1. Using the FSM in a Component. This means to handle the HTML in the component and transparently call it with the DataHandler. In fact: The Component would extends the DataHandler. Then the DataHandler would become a DataBroker. :arrow_right: some kind of extendable ViewModel
  2. Using the FSM directly as an Component. So the Component would be a Model without any HTML Relation. Eg. TYPO3/CMS/FrontendEditing/Component/Presentation/ToggleState In fact: The combined states handling (eg. ['panelOpen', 'fullscreen']) has to get somewhere. I see following options:
    • create combined FSM and use transition in events. eg: add state disabled and transition to it from state panelClosed listening on fullscreen while panelOpen is goin into state hidden
    • (simillary to the previous one) combine FSM and use transition in events. eg: add state fullscreen and add transition hideButton to it from state panelClosed listening on fullscreen while panelOpen has transition hide
    • create different type of DataHandler. Eg. AnimationDataHandler
    • handle it outside, like in CSS. .panelOpen.fullscreen{...} .panelOpen{...} .fullscreen{...}

I think solution 1. would lead to create a DataHandler for every Component. This gives us an extra portion flexibility but i guess this is mostly not needed.

Would love to hear from you.

edit on 2021-03-25: added a show case branch an made git pages of this issue

MattiasNilsson commented 3 years ago

@Messj1 I think solution 1 is the best to go with. Then it feels more organised :)