-
Notifications
You must be signed in to change notification settings - Fork 12
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
UI libraries architecture #133
Comments
|
It's an edge case, so after giving it more thought I'm about to ignore it :D. The case looks like this – since all the installed (as packages) UI libraries are available somewhere in the build directory (e.g. as But as I said – it's a super edge case. And given that it's quite unlikely that there will be many UI libraries out there (people will rather focus on theming the default one), that becomes even less probable. Hence, I don't think it matters. And yes – I think that the general rule should be that features use components from
That was a different issue – ckeditor/ckeditor5#50 – and it was about removing files which are versioned for each format (amd, cjs, esnext). Not related to bundling. The builder, in general, does not care about which files will be used or not as it doesn't know this. The bundler will need to make this decision. So far it's easy – take I want to keep these rule, so that's why I propose that the features will import the UI component that they want to use. This gives us a straight info – the developer wants this feature and this feature requires this UI component. Note how easily this nice state could've been destroyed. We were considering having a repository of UI components. There would need to be a registry of them. The feature would then say "I want to create a 'button' type of component". Nice, right? But the registry of these components would need to import all them, so the bundler would always think that all components are in use.
I think that too. In general, we are only opening doors for other UI libraries, but we don't have to optimise every possible, strange scenario. |
I knew that's the case. This is kinda defeating the purpose of decoupled UI library. I don't think that any developer should ever decide that this is the only UI lib that should be used with my feature. If components for that feature are available in overriding UI lib, they should be used. Otherwise, it will lead to strange scenarios where some part of UI is different than other. I think that forcing importing UI from |
I started implementing the component registry. So the place where features register their buttons and they can be instantiated in other places. I have this: export default class ComponentRepository {
constructor( editor ) {
/**
* @readonly
* @type {core.Editor}
*/
this.editor = editor;
this._components = new Map();
this._instances = new Set();
}
add( name, ControllerClass, ViewClass, model ) {
this._components.set( name, {
ControllerClass,
ViewClass,
model
} );
}
create( name ) {
const component = this._components.get( name );
const model = component.model;
const view = new component.ViewClass( model );
const controller = new component.ControllerClass( this.editor, model, view );
// Ouch... this will leak the memory. How can we keep the instances to avoid that?
// (No, weak maps are not the answer here).
this._instances.add( {
name,
view,
controller,
model
} );
return controller;
}
} See the comment inside the We talked with @fredck that it would be nice if someone could retrieve all instances of a specific button (e.g. all bold buttons) in order to extend their behaviour in some way. In order to do that, we need to store instances of those components. However, doing this like above will lead to a memory leak. E.g. the buttons will stay in the memory even though the toolbar containing them was destroyed. I think though that this could be solved by introducing a However, I'm not going to implement this for now. I don't want to waste time on this as the case is quite edgy anyway. |
To clarify this – I don't think that anyone should be doing this, but well... we can expect the developer will try to extend existing functionality in this way. |
I've pushed initial implementation (see ckeditor/ckeditor5-core#209 (comment)). I think that there's nothing more I can implement now, because things like feature localization isn't defined, neither are features (see #129). I'm going to write some docs and tests and push it on review. |
I've discussed with some of you the shape of the first implementation and we came up with some changes. Some things in the initial implementation didn't have much sense (mainly - code placement), but I think that it's now more clear what should land where. I've put some proposal in https://github.com/ckeditor/ckeditor5-design/wiki/UI-Library. At the end it contains some ideas that may be worth considering. I'd like to make a decision about https://github.com/ckeditor/ckeditor5-design/wiki/UI-Library#directory-structures asap, because I'll be making other changes in the library and I can do this as well. |
I know that
is simpler, but the architecture must allow
in case some component has extra dependencies, static libraries etc. Otherwise, a single exception to the simple structure will end up with a mess because children of
So to KISS I'd simply stay with |
+1 |
When it comes to 3rd Party UI Components (btw, it should be "Third-party"), we may do it by convention (fixed directory name, like
|
As for the rest, it looks great for me. |
Thanks! The third-party UI components worries me though, because that before-great architecture becomes a small mess after considering them :D. I think that a requirement that we could make is that additional UI components, which are meant to become part of the UI lib, should be defined in a separate Another thing is that exposing UI components in packages has a bit higher chance of avoiding name collisions because developers will have an easier job of controlling what UI components are getting into the build. Having this requirement, the builder could do the following:
I think that it will create the correct structure. And the developer will have an option to fine-tune the order by the builder configuration (moving more packages from 2nd to 3rd step). But it's extremely unlikely that anyone will ever need to do this, because, let's be honest – extensions for third-party UI components will not happen that often :D. |
BTW. That building steps may look easy to implement, but there'll be a lot of work to do so. Especially the watcher's implementation will require an algorithm for resolving whether a file coming from some package would be the last version copied to |
+1 for #133 (comment) |
Cleaning up old discussions. See #186. |
It's high time to discuss how the UI libraries will work in CKEditor 5. All we know so far is that the core UI library will be a simple implementation of the MVC pattern what will decouple views from the rest of the code. This will satisfy (from the code perspective) the most important requirement which is that developers will be able to replace the default UI library with their implementations.
I want to approach the design of this architecture based on two most important use cases: the toolbar case and typical UI components like autocompleter or dialogs. Those cases are different in this sense, that in the former one the feature does not instantiate the buttons by itself, it only registers them, while in the latter the feature does all the job.
Specialisation
Layers of specialisation:
Requirements
Requirements for the UI libraries architecture:
Code Split
Let's also look at the picture from a different perspective – where does the code of the MVC triad of a UI component come from?
Implementation
The first requirement, which is that the UI library must not break optimised builds means that features must directly import UI components' views, controllers and models (if provided).
Features need to use some predictable paths to the UI library, so whichever library the developer chose, the path must be the same. Therefore, we propose that the chosen library is copied by the builder to the
dist/<format>/ui/
directory (make sure to read the Building article).Now, that library (e.g.
ckeditor5-ui-my
) needs to be able to import modules from the default library (ckeditor5-ui-default
). This means that its code must also be available in thedist/<format>/
directory. It's easiest to simply have it under its usual directory which would bedist/<format>/ui-default/
.And the last fact – the chosen UI lib may not implement the whole UI library, but just a couple components, just the views, or something. The features must still be able to import the missing pieces, so they need to be available in
dist/<format>/ui/
as well.Hence, the algorithm of the builder:
ckeditor5-ui-default
package will land inui-default/
andckeditor5-ui-my
inui-my/
ui-default/
toui/
directory.ui/
directory (override existing files).Now the feature's code:
Now, let's consider where will the button get it localized label. The
label: 'bold'
property of the model defines only the language string key. Its value will need to be taken e.g. fromeditor.lang.bold
.That will be done by the controller of the button. As mentioned in "Code Split", the default library will define this binding. This means that the feature components registry must pass the editor to the controller when instantiating it.
The toolbar (or a creator) will later get a bold button instance in the following way:
Notes
The steps 2-3 of the building algorithm can be defined as an array of packages which need to be copied to
ui/
one by one. This will allow to compose the UI library out of many packages.Let's assume we have a
ckeditor5-ui-autocompleter
package which defines an additional UI component. Do we want to make it re-definable by other UI libraries? I think so, otherwise other UI libraries will not be able to affect it easily – forks of the original UI component will need to be created and somehow injected in place of the orignal component (in its directory, because features will look of it in a specific way). That could even work, but then the initial code would not be reusable (because its directory is already occupied).Therefore I think that it should work differently – the package can somehow notify the builder that it contains an UI component and it would be unpacked inside
ui/<provided-name>/
. Then, other UI libraries will be able to contain also an extension for it.If a component will not provide a special model class, then this means that other UI lib extension will not be able to define the model either because features will not start using it automatically. I think though though that it makes a perfect sense because the model is the interface of the component so it cannot be changed.
The builder will copy some code to two directories (e.g. the default UI lib will be in
ui/
andui-default/
). I initially thought that if the features will use the UI library only throughui/
then it's OK, because the duplicates are not used. But neither it is completely true, nor we can say that features are only allowed to use what's inui/
. On the other hand, this is a very specific case, so perhaps we can live with a small code duplication?Alternatively, what if we do all the operations virtually, by changing the import paths? This would be tricky, because the builder would need to check which UI lib contains which files, to know what's the most important version of a specific file. We would also lose the nice touch of the first solution which is that you can see all the code in your file system and even do some overrides manually. It should avoid any code duplication though.
Uh... Questions? :D
The text was updated successfully, but these errors were encountered: